做施工排期、预算和合同管理的 Reperline 团队,把 Claude 接进了系统。项目经理可以问"哪些里程碑延期了",也可以上传一张发票,让 Claude 先把录入条目准备好。
问题就出在第二步。预算数据是钱。团队不想把"AI 只提建议、不改数据"这句话,寄托在应用代码的自觉上——一次重构就可能把它弄坏。他们要的是数据库层面根本做不到。
做法是给 Web 应用和 MCP 服务器分配两个不同的 Postgres 角色,两个角色都没有 BYPASSRLS 权限。
- app_rw:Web 应用使用,可读可写,受行级安全策略过滤。
- app_mcp:MCP 服务器使用,只能 SELECT(行级安全照样生效),并且只能 INSERT 进一张表:pending_changes。
给 app_mcp 的授权只有一句:grant insert on pending_changes to app_mcp。配套的插入策略要求 org_id 等于当前组织、status 为 pending、source 为 claude。
整个系统里,app_mcp 没有任何 UPDATE 或 DELETE 授权。所以哪怕某个工具处理函数出了 bug,或者提示词把模型说服了去尝试,它能做的最坏情况也只是在自己组织里塞进一行"待处理、来自 Claude"的记录。这行记录要由人在收件箱里批准或驳回。
真正执行变更的函数 apply_pending_change 只有 app_rw 能调用,而且刻意设为 SECURITY INVOKER:它以批准者本人的行级安全身份运行,一个人只能批准他自己本来就有权做的改动。
一个默认设置,悄悄拆掉了半道墙
系统里有些写入走函数,比如应用导入的排期。早期的一次迁移里,团队写了类似这样一句:revoke execute on function apply_schedule_import(uuid) from app_mcp;
它什么也没做。在 Postgres 里,新建函数默认对 PUBLIC 可执行,而每个角色都是 PUBLIC 的成员。从 app_mcp 撤销,撤掉的是它本来就不单独持有的授权,PUBLIC 那道授权仍然放它进来。
修复写在下一版迁移里:先把相关函数从 public 撤销,再单独授权给 app_rw。此后敏感函数一律先从 public 撤销,只授予真正需要的那一个角色。团队的建议是:如果你靠角色来限制 AI 代理,别只看表,也要看函数。has_function_privilege 可以放进 CI 做快速检查。
权限当数据存,每条语句只算一次
角色按组织可配置,策略里就不能写死角色名。一个权限是"区域 + 读或写"的组合,策略调用 has_perm(area, mode),这是一个 SECURITY DEFINER 函数。
有个细节值得记:调用要包在标量子查询里,写成 (select has_perm('budget', 'r'))。直接裸写,查询规划器可能对每一行都调用一次;包成子查询后它不依赖当前行,Postgres 会把它当作 init plan,每条语句只求值一次。
必须永远成立的规则,用 RESTRICTIVE
Postgres 把普通(permissive)策略用 OR 组合。这对"负责人或财务可读"是对的,对"只能看你被分配的项目""试用期结束后只读"这类必须叠加在所有规则之上的约束就是错的。
这类规则用 RESTRICTIVE 策略,以 AND 组合,按表生成。好处是后来新增的 permissive 策略无法把权限放宽到它们之外——这正是"试用期后只读"这种规则需要的效果。
一个自己找新表的测试
行级安全最薄弱的地方,是下个月有人新建了一张表却忘了保护。团队的隔离测试不维护清单,而是直接读系统目录,找出所有带 org_id 列的表和视图,建两个组织,再分别以 app_rw 和 app_mcp 的身份验证:A 组织的用户读不到、改不了、删不掉、也插不进 B 组织的任何数据。新表一存在就被覆盖,策略缺失就直接让测试失败。
数据库管不了的那部分
授权和策略决定谁能改什么,决定不了 Claude 提的建议对不对。一张发票读错了,提案依然是错的;它只是没法在没人看一眼的情况下进到账里。真正的控制点在收件箱,数据库只保证一件事:没有人能绕过它。
如果要把大模型接到重要的数据上,这个团队的建议只有一条:给它一个独立的数据库角色,逐条列出这个角色能写什么,然后像攻击者那样去测它,函数也要测。
热门跟贴