版本
@objectstack/{spec,runtime,console}@17.0.0-rc.0
期望(按 spec 注释)
src/ui/action.zod.ts 里 requiredPermissions 的注释写明这是 ADR-0066 D4 的双面门禁:
System capabilities required to INVOKE this action — a dual-surface gate from ONE declaration: the server (action route) rejects the call with 403 when the caller's systemPermissions don't cover these (the source of truth), and the objectui ActionRunner hides/disables the button using the same requirement.
即:一处声明 → 服务端 403 + 客户端隐藏/禁用按钮。配套的授予侧是权限集的 systemPermissions(src/security/permission.zod.ts:279)。
实测结果:按钮照常显示、照常可点
复现(本地 dev,objectstack dev,Console 以内置管理员登录):
- 给某对象的
record_header 动作加一个谁都没有的能力:
{name: 'set_parallel_positions',label: '设置并行会审岗位',locations: ['record_header'],type: 'api',method: 'POST',target: '/api/v1/ts/record-action/<obj>/${param._rowRecord.id}',requiredPermissions: ['ehr_probe_capability'],// 没有任何权限集声明过它visible: "record.approval_stage == 'qc_director' && record.approval_status == 'pending'",}objectstack compile → 打开该记录详情页。- 按钮正常出现在页头,可点、可提交。
原因(两处)
① 客户端 fail-open
packages/console 打包产物 dist/assets/framework-*.js 的 getActionsForLocation:
.filter(e=>{lett=e.action.requiredPermissions;if(!Array.isArray(t)||t.length===0)returntrue;letn=this.runner.getContext()?.user?.systemPermissions;return!Array.isArray(n)||t.every(e=>n.includes(e));// ← 拿不到就放行})会话里没有 systemPermissions 时直接放行。对「隐藏按钮」这类安全语义,fail-open 等于门禁不存在。
② 会话根本没带 systemPermissions
同一页面里 Console 连续两次 POST /api/v1/security/explain → 401 Unauthorized(已登录的 admin 会话),客户端上下文的 user 上也取不到 systemPermissions。服务端 resolveAuthzContext 明明解析出了 { positions, permissions, systemPermissions }(packages/runtime dist index.js:5155 附近),但这份信息没有到达 Console。
附带:服务端那半边并不覆盖所有 action
actionPermissionError 只在平台自己的 action 调用路径上生效(packages/runtime dist index.js:2922 / 4393 / 4752,即 ql.executeAction 路由与 MCP/AI 路径)。type: 'api' 指向自建 endpoint 的动作不经过它,因此这类动作即使声明了 requiredPermissions 也没有任何服务端兜底。spec 注释里「the server (action route) rejects…the source of truth」的措辞容易让人误以为任何 action 都被兜底。
影响
requiredPermissions 是目前唯一的声明式「按能力/岗位控制按钮显隐」手段——动作的 visible 只绑 record 命名空间(写 has(current_user.id) 直接编译失败:bare reference current_user — … binds the record as the record namespace, not at top level;运行期实测 has(user.id) / has(user.permissionSets) / has(user.positions) 的探针按钮全部不显示,只有 visible: 'true' 显示)。这条路一旦空转,应用侧就只能自己写 Hook 在服务端兜底,UI 上无法把按钮对无权用户藏起来。
建议
- 会话(或
/api/v1/security/explain)向 Console 下发调用方的 effective systemPermissions,并修掉已登录会话下 security/explain 401。 - 客户端在拿不到
systemPermissions 时 fail-closed(或至少提供显式配置),避免「声明了门禁但静默失效」。 - 文档/注释明确:服务端 403 只覆盖平台 action 路由,
type: 'api' 自建 endpoint 需自行校验。
版本
@objectstack/{spec,runtime,console}@17.0.0-rc.0期望(按 spec 注释)
src/ui/action.zod.ts里requiredPermissions的注释写明这是 ADR-0066 D4 的双面门禁:即:一处声明 → 服务端 403 + 客户端隐藏/禁用按钮。配套的授予侧是权限集的
systemPermissions(src/security/permission.zod.ts:279)。实测结果:按钮照常显示、照常可点
复现(本地 dev,
objectstack dev,Console 以内置管理员登录):record_header动作加一个谁都没有的能力:objectstack compile→ 打开该记录详情页。原因(两处)
① 客户端 fail-open
packages/console打包产物dist/assets/framework-*.js的getActionsForLocation:会话里没有
systemPermissions时直接放行。对「隐藏按钮」这类安全语义,fail-open 等于门禁不存在。② 会话根本没带 systemPermissions
同一页面里 Console 连续两次
POST /api/v1/security/explain → 401 Unauthorized(已登录的 admin 会话),客户端上下文的user上也取不到systemPermissions。服务端resolveAuthzContext明明解析出了{ positions, permissions, systemPermissions }(packages/runtimedistindex.js:5155附近),但这份信息没有到达 Console。附带:服务端那半边并不覆盖所有 action
actionPermissionError只在平台自己的 action 调用路径上生效(packages/runtimedistindex.js:2922 / 4393 / 4752,即ql.executeAction路由与 MCP/AI 路径)。type: 'api'指向自建 endpoint 的动作不经过它,因此这类动作即使声明了requiredPermissions也没有任何服务端兜底。spec 注释里「the server (action route) rejects…the source of truth」的措辞容易让人误以为任何 action 都被兜底。影响
requiredPermissions是目前唯一的声明式「按能力/岗位控制按钮显隐」手段——动作的visible只绑record命名空间(写has(current_user.id)直接编译失败:bare reference current_user — … binds the record as the record namespace, not at top level;运行期实测has(user.id)/has(user.permissionSets)/has(user.positions)的探针按钮全部不显示,只有visible: 'true'显示)。这条路一旦空转,应用侧就只能自己写 Hook 在服务端兜底,UI 上无法把按钮对无权用户藏起来。建议
/api/v1/security/explain)向 Console 下发调用方的 effectivesystemPermissions,并修掉已登录会话下security/explain401。systemPermissions时 fail-closed(或至少提供显式配置),避免「声明了门禁但静默失效」。type: 'api'自建 endpoint 需自行校验。