在为 #3580 做浏览器验证时顺带发现的。不是 #3580 引入的,是既有行为。
✅ 已由 #3599 修复(已合并)
答案是第 3 种:租户级可见不是有意的设计,而是「绕过 RLS 之后只实现了一半」。
buildRequestWhere 的注释自己点明了这一点:
we deliberately query with SYSTEM_CTX to bypass RLS on the engine (the approver-visibility rule spans three identity forms, which RLS can't model cleanly)
——注释点名了审批人可见性规则,但代码只写了租户那一半,参与者那一半从未落地。
修复后的规则
一条请求对它的参与者可见:提交人 / 当前审批人(走归一化审批人索引,覆盖写路径记录的每种身份形态)/ 已经在其上行动过的人(槽位已流转的历史审批人、评论者)。具备 override 权限的管理员保留不受限视图;无 token 的上下文什么都看不到。
approverId 明确定性为过滤器,不是授权。
为什么用具体 user id 就够
这点值得记下来,因为这里授权不足会静默隐藏别人必须处理的审批——比泄漏更难发现:
- position / team / manager / field 类审批人在开单时就已解析成具体 user id
type:value 字面量只是解析到「无人」时的兜底,那种槽位任何人都无法行动(can_act 就是对已解析 id 的朴素成员测试)
所以这条规则不可能把请求从真正能处理它的人眼前藏起来。
一个实现上的坑(值得记录)
每个写操作都通过 getRequest 回读它刚改过的请求作为返回值。第一版实现把这条路径也判了权,结果对不带 userId 的上下文(flow 驱动的 resume、服务间调用)会把一次成功的写入变成 null。4 个既有测试正好抓到了它 —— 回读现在走不判权的 readBackRequest,因为操作在写之前已经用自己的规则授过权。
浏览器验证(app-showcase)
| 场景 |
修复前 |
修复后 |
| Ada(非管理员,只是 expense 的审批人)不带过滤器列请求 |
3 条(整个租户) |
1 条(只有她的) |
| Dev Admin 列请求 |
3 条 |
3 条(控制台视图保留) |
| Ada 下载自己经手请求的附件 |
200 |
200,真实 PDF(#3580 正常复合) |
| Ada 读她无关请求的时间线 |
200,2 行 |
空 |
第 2 个问题的答案
「直接读被 PERMISSION_DENIED、经服务读却放行」这个不一致依然存在,但现在方向是对的:服务侧的规则比对象权限更精确(它能表达 RLS 表达不了的审批人身份),而不是更松。这是有意的分工,已在 content/docs/automation/approvals.mdx 中写明谁能看到什么。
相关:#3580(文件读权限委托)、#3505(决策附件的名称/下载)。
在为 #3580 做浏览器验证时顺带发现的。不是 #3580 引入的,是既有行为。
✅ 已由 #3599 修复(已合并)
答案是第 3 种:租户级可见不是有意的设计,而是「绕过 RLS 之后只实现了一半」。
buildRequestWhere的注释自己点明了这一点:——注释点名了审批人可见性规则,但代码只写了租户那一半,参与者那一半从未落地。
修复后的规则
一条请求对它的参与者可见:提交人 / 当前审批人(走归一化审批人索引,覆盖写路径记录的每种身份形态)/ 已经在其上行动过的人(槽位已流转的历史审批人、评论者)。具备 override 权限的管理员保留不受限视图;无 token 的上下文什么都看不到。
approverId明确定性为过滤器,不是授权。为什么用具体 user id 就够
这点值得记下来,因为这里授权不足会静默隐藏别人必须处理的审批——比泄漏更难发现:
type:value字面量只是解析到「无人」时的兜底,那种槽位任何人都无法行动(can_act就是对已解析 id 的朴素成员测试)所以这条规则不可能把请求从真正能处理它的人眼前藏起来。
一个实现上的坑(值得记录)
每个写操作都通过
getRequest回读它刚改过的请求作为返回值。第一版实现把这条路径也判了权,结果对不带userId的上下文(flow 驱动的 resume、服务间调用)会把一次成功的写入变成null。4 个既有测试正好抓到了它 —— 回读现在走不判权的readBackRequest,因为操作在写之前已经用自己的规则授过权。浏览器验证(app-showcase)
第 2 个问题的答案
「直接读被
PERMISSION_DENIED、经服务读却放行」这个不一致依然存在,但现在方向是对的:服务侧的规则比对象权限更精确(它能表达 RLS 表达不了的审批人身份),而不是更松。这是有意的分工,已在content/docs/automation/approvals.mdx中写明谁能看到什么。相关:#3580(文件读权限委托)、#3505(决策附件的名称/下载)。