Henrietta Dombrovskaya 在 Estonia Postgres User Group 的分享,表面上是一场关于 pg_acm 的技术演讲,真正有意思的地方却在反馈:很多听众在茶歇时直接说“这些问题我都遇到过”。这说明它不是一个只属于 DBA 的冷门话题,而是应用开发者每天都在碰到的权限边界、数据访问和业务规则落地问题。
pg_acm 解决的不是“权限表好不好看”
从分享反馈看,pg_acm 之所以能让应用开发者快速产生共鸣,是因为它触到的是应用系统里最难长期维护的一类逻辑:谁能看什么、谁能改什么、权限如何跟业务状态一起变化。
很多团队一开始会把访问控制写在应用层:
- Controller 里判断用户角色;
- Service 里判断记录归属;
- SQL 查询里拼接
tenant_id或owner_id; - 后台脚本里再复制一遍类似规则。
短期看很快,长期看会变成散落的条件判断。更糟的是,报表、批处理、临时查询、数据修复脚本往往绕过应用层入口,权限模型很容易出现裂缝。
pg_acm 这类思路的价值在于:把访问控制问题拉回数据所在的位置讨论。不是说所有业务规则都必须塞进数据库,而是让数据库承担它天然擅长的部分:靠近数据、统一执行、减少绕路。
为什么应用开发者比许多 DBA 更快“get it”
原文里提到一个很有意思的观察:应用开发者往往比许多 DBA 更快理解这种方法的优势。这并不奇怪。
应用开发者每天面对的是完整业务路径:登录用户、组织关系、页面按钮、API 返回、审计记录、后台任务。对他们来说,访问控制不是抽象的 GRANT SELECT,而是一个具体问题:
这个用户能不能看到这张订单?如果订单进入归档状态,还能不能修改?如果用户属于多个组织,查询结果应该是什么?
传统数据库权限通常更偏对象级别,比如表、视图、函数。而应用访问控制常常是行级、字段级、状态级、关系级。两者之间有一段灰色地带,正是许多系统越做越复杂的来源。
因此,当一个工具或方案试图把这些规则结构化、集中化,并且让 PostgreSQL 参与执行,应用开发者会立刻想到自己代码库里那些“再也不敢碰”的权限判断。
可以这样实践:用 PostgreSQL RLS 模拟同类问题
如果你还没有使用 pg_acm,也可以先用 PostgreSQL 自带的 Row Level Security(RLS)感受这类方案的边界。下面示例不是原演讲中的 pg_acm API,而是一个可复制运行的最小实验,用来模拟“租户只能访问自己的订单”。
运行前需要本机有 PostgreSQL,并能用 psql 连接数据库。把 app.tenant_id 改成你的实际租户标识即可。
DROP TABLE IF EXISTS orders;
DROP ROLE IF EXISTS app_user;
CREATE ROLE app_user LOGIN PASSWORD 'app_user_password';
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_id text NOT NULL,
amount numeric(12, 2) NOT NULL,
status text NOT NULL DEFAULT 'open'
);
INSERT INTO orders (tenant_id, amount, status) VALUES
('tenant_a', 120.00, 'open'),
('tenant_a', 88.50, 'paid'),
('tenant_b', 42.00, 'open');
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
FOR SELECT
USING (tenant_id = current_setting('app.tenant_id', true));
GRANT SELECT ON orders TO app_user;
然后用同一个连接模拟不同租户访问:
psql "postgresql://app_user:app_user_password@localhost/postgres" <<'SQL'
SET app.tenant_id = 'tenant_a';
SELECT id, tenant_id, amount, status FROM orders ORDER BY id;
SET app.tenant_id = 'tenant_b';
SELECT id, tenant_id, amount, status FROM orders ORDER BY id;
SQL
你会看到同一条 SQL 在不同会话变量下返回不同数据。这就是把访问边界放进数据库执行路径后的直观效果:应用仍然负责认证、会话和业务流程,但数据库可以成为最后一道一致性闸门。
如果继续扩展,可以这样改造:
CREATE POLICY tenant_update_open_orders ON orders
FOR UPDATE
USING (
tenant_id = current_setting('app.tenant_id', true)
AND status = 'open'
)
WITH CHECK (
tenant_id = current_setting('app.tenant_id', true)
AND status = 'open'
);
GRANT UPDATE (amount, status) ON orders TO app_user;
这段策略表达了一个更贴近业务的限制:只能更新当前租户下、仍处于 open 状态的订单。真实项目里,你还需要处理角色继承、管理员例外、审计、迁移脚本和连接池变量污染等问题。
开源版本的现实:反馈越多,越需要工程化收口
原文提到,现场反馈让作者意识到需要修复开源版本里的 bug,并继续扩展即将在 PG.Conf EU 使用的版本。这一点很关键:访问控制工具不是写一个漂亮 DSL 就结束了,它必须接受真实系统的压力测试。
尤其要关注这些边界:
- 规则是否容易审计,还是只能靠作者解释;
- 错误配置是否默认拒绝,而不是默认放行;
- 迁移时旧规则和新规则如何并存;
- 连接池是否会复用上一个请求的上下文变量;
- 批处理、报表、管理员后台是否走同一套访问路径;
- 性能是否能被
EXPLAIN、索引和测试稳定验证。
这也是 Tallinn 这场 meetup 的价值:不是“大家觉得演讲不错”,而是使用者用自己的事故经验验证了问题存在,并愿意试用一个解决方向。
采用建议:先把权限问题画出来,再引入工具
如果你的团队也被访问控制逻辑拖慢,不要急着把所有规则迁到数据库。更稳妥的做法是从一个高风险、规则清晰、数据范围可控的模块开始。
可以按这个清单推进:
- 选一个实体,例如订单、项目、工单或文档;
- 写出当前所有读取、更新、导出、后台处理路径;
- 标出哪些路径依赖应用层判断,哪些路径会直接访问数据库;
- 用 RLS 或
pg_acm这类方案做一个最小闭环; - 为权限规则补上集成测试和
EXPLAIN性能检查; - 明确管理员、迁移脚本和故障修复流程的例外机制。
pg_acm 这类工具真正吸引人的地方,不是让数据库替代应用,而是让访问控制从“散落在代码里的约定”变成“可以讨论、可以测试、可以演进的系统部件”。这也是为什么一场看似小众的 Postgres 演讲,会让现场应用开发者不断点头。