4.6 KiB
PostgreSQL 角色与 RLS 执行约束
1. 目标
数据库迁移和日常应用访问必须使用不同的登录身份,避免应用连接碰巧成为表所有者而静默绕过工作空间 RLS。
| 身份 | 用途 | 表所有权 | 日常数据访问 |
|---|---|---|---|
atlas_migrator |
执行 schema migration | 拥有 atlas schema 和其中的表 |
仅部署时使用 |
atlas_app_login |
API、BullMQ Worker 连接数据库 | 不拥有任何业务表 | 通过 atlas_app 获得最小 DML 权限,受 RLS 限制 |
atlas_app |
无登录的权限组角色 | 不拥有任何业务表 | 承载 API/Worker 的表和序列权限 |
atlas_app_login 和 atlas_app 都必须是 NOBYPASSRLS,也不能是 PostgreSQL 超级用户。生产环境不得把 atlas_migrator 的连接串写入 API 或 Worker 的环境变量。
角色链与请求上下文
三层角色链是固定的,不能简化为“应用直接使用迁移账号”:
atlas_migrator ──拥有──> atlas schema 与业务表
atlas_app_login ──登录身份,NOINHERIT──> SET LOCAL ROLE atlas_app
atlas_app ──无登录权限组──────────> 最小 DML 与序列权限
API 和 Worker 只以 atlas_app_login 建立连接。每笔业务事务必须先执行
SET LOCAL ROLE atlas_app,再设置 SET LOCAL app.workspace_id。前者收窄为
应用所需的最小数据库权限,后者为 RLS 策略提供当前工作空间上下文;任何一步缺失,
都不应使跨工作空间数据变得可见。
2. 一次性数据库初始化
以下由云数据库管理员或基础设施脚本执行;它不属于 NestJS 的业务 migration,因为托管 PostgreSQL 常不允许应用账户创建角色。密码应从密钥管理服务注入,示例中不填写真实密码。
CREATE ROLE atlas_migrator LOGIN NOINHERIT NOBYPASSRLS PASSWORD '<managed-secret>';
CREATE ROLE atlas_app NOLOGIN NOINHERIT NOBYPASSRLS;
CREATE ROLE atlas_app_login LOGIN NOINHERIT NOBYPASSRLS PASSWORD '<managed-secret>';
GRANT atlas_app TO atlas_app_login;
GRANT CREATE ON DATABASE atlas TO atlas_migrator;
CREATE SCHEMA IF NOT EXISTS atlas AUTHORIZATION atlas_migrator;
GRANT USAGE ON SCHEMA atlas TO atlas_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA atlas TO atlas_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA atlas TO atlas_app;
ALTER DEFAULT PRIVILEGES FOR ROLE atlas_migrator IN SCHEMA atlas
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO atlas_app;
ALTER DEFAULT PRIVILEGES FOR ROLE atlas_migrator IN SCHEMA atlas
GRANT USAGE, SELECT ON SEQUENCES TO atlas_app;
首次 DDL 与后续 migration 都以 atlas_migrator 执行,确保新表的所有者一致。迁移角色拥有数据库级 CREATE 的唯一目的,是创建其拥有的 atlas schema;它不授予 API 或 Worker 任何额外权限。若云服务统一由管理员创建 schema,可将 atlas_migrator 设为 schema/表所有者或由管理员代为执行 migration;无论哪种方式,应用登录角色都不得成为表所有者。
3. RLS 的运行方式
迁移为每张带 workspace_id 的业务表执行:
ALTER TABLE atlas.example ENABLE ROW LEVEL SECURITY;
ALTER TABLE atlas.example FORCE ROW LEVEL SECURITY;
FORCE 会让表所有者的普通 SELECT / INSERT / UPDATE / DELETE 也遵守策略,避免错误配置在通常情况下被掩盖。PostgreSQL 超级用户和拥有 BYPASSRLS 属性的角色仍能绕过它,因此它们只可用于受控运维,不可用于 API 或 Worker。
API 的数据库事务顺序固定为:验证登录用户与工作空间成员关系 → 开启事务 → SET LOCAL app.workspace_id = '<workspace UUID>' → 执行业务 SQL → 提交/回滚。SET LOCAL 只在当前事务内有效,连接归还连接池后不会遗留工作空间上下文。
Worker 没有浏览器会话,也必须先从受控任务记录解析出工作空间,再在每一笔业务事务中设置相同的 app.workspace_id。跨工作空间的运维任务使用专门的受控运维连接,不复用应用连接池。
4. 部署检查
- 使用迁移连接串运行 DDL;使用应用连接串启动 API 与 Worker。
- 在 staging 中用应用连接验证:未设置
app.workspace_id时查询不到业务数据;设置 A 工作空间后不能读取 B 的记录。 - 检查
pg_roles.rolbypassrls,确保应用角色为false;检查pg_tables.tableowner,确保应用角色不是业务表所有者。 - 业务服务仍要做成员授权、资源归属和平台/门店校验;RLS 是纵深防御,不是授权逻辑的替代品。