3.4 模式、所有权与对象边界
关系模型还需要命名与权限边界。PostgreSQL schema 是数据库内的 namespace,也参与权限解析;它不是独立数据库、租户隔离或微服务边界的自动实现。边界成立要靠所有权、USAGE、对象权限和受控名称解析共同支持。
3.4.1 业务模式、接口模式与内部模式
v0 使用三个 schema:
| schema | 内容 | 谁需要 USAGE | 承诺 |
|---|---|---|---|
shop | customer、product、order、line、payment 规范关系 | app、只读角色 | 业务事实与命令模型 |
shop_api | order_summary 等显式查询接口 | app、只读角色 | 面向消费者的读取形状 |
shop_private | 未来内部 helper、staging 或实现对象 | owner | 无外部兼容承诺 |
public 仍存在,但 ch01 已撤销 PUBLIC 的 CREATE。本书不把业务对象默认堆进 public,以免命名、权限与扩展对象混杂。
schema 是 namespace
同一数据库可以同时存在:
shop.customer
crm.customer
archive.customer未限定的 customer 由 search_path 决定。显式 shop.customer 同时表达对象和边界,迁移、审计和安全敏感 SQL 应优先使用。
schema 不能提供:
- 独立 WAL、备份或故障域;
- 独立连接、资源隔离或主要版本;
- 自动跨租户行隔离;
- 自动阻止拥有更高数据库权限的角色访问。
需要这些性质时,应评估数据库、集群、RLS、资源治理或服务边界,而不是给 schema 换一个更宏大的名字。
接口 view 不是天然安全 view
shop_api.order_summary 把五表投影为读取形状。v0 的 app 和只读角色本来就有底表 SELECT,因此该 view 只表达接口与派生逻辑,不承担隐藏敏感行的安全责任。
PostgreSQL 普通 view 默认按 view owner 的底层权限检查。若想用 view 作为安全边界,还要审查 security_barrier、security_invoker、函数是否 leakproof、RLS 与调用者可创建对象的权限。ch23 专门处理;本章绝不因为“只 grant 了 view”就宣称数据已隔离。
接口演进需要兼容契约
CREATE OR REPLACE VIEW 不能任意改变既有列名称、顺序和类型;新查询必须保留现有列,最多在末尾增加列。消费者依赖哪些列、空值和排序,应在 ch12 的服务契约中管理。
本章 view 查询不承诺默认排序。调用方必须显式 ORDER BY。
3.4.2 对象所有者与运行角色分离
对象 owner 可以修改、删除对象和转授权限,是结构控制身份;应用只应拥有完成运行任务所需的 DML。v0 角色:
| 角色 | LOGIN | 责任 |
|---|---|---|
pg36_owner | 否 | 拥有 schema、table、view,执行受控迁移 |
pg36_app | 是 | 读取与修改规范业务关系 |
pg36_ro | 是 | 只读规范关系和接口 view |
dbuser_dba | 是 | L1 管理入口;经授权 SET ROLE pg36_owner |
DDL 脚本先以管理员认证,再:
SET ROLE pg36_owner;于是新对象直接归 pg36_owner,而不是先由个人管理员拥有再批量改 owner。NOLOGIN owner 没有可泄露的直接登录凭据。
owner 权力不是普通 ACL
对象 owner 隐含拥有改变、删除和授权对象的能力。即使撤销 owner 的普通 SELECT,owner 仍能重新 grant。因此安全设计不能把“owner 账号”当作日常应用角色。
超级用户又能绕过绝大多数权限边界。L1 的 dbuser_dba 只用于教学管理;生产迁移应采用受审计、短时授权和明确发布流程。
显式权限与默认权限
setup 对当前五表执行:
GRANT SELECT, INSERT, UPDATE, DELETE
ON TABLE shop.customer, shop.product, shop.sales_order,
shop.sales_order_item, shop.payment
TO pg36_app;
GRANT SELECT ON TABLE ...
TO pg36_ro;并为 owner 在 shop_api 的未来关系设置默认 SELECT。ALTER DEFAULT PRIVILEGES 只影响以后由指定 owner 创建的对象,不会回填现有对象,也不会自动适用于另一创建角色或 schema。
验证实际边界:
SELECT
has_table_privilege(
'pg36_app', 'shop.sales_order', 'INSERT'
) AS app_can_insert,
has_table_privilege(
'pg36_ro', 'shop.sales_order', 'SELECT'
) AS ro_can_select,
has_table_privilege(
'pg36_ro', 'shop.sales_order', 'UPDATE'
) AS ro_can_update,
has_schema_privilege(
'pg36_app', 'shop_private', 'USAGE'
) AS app_can_use_private;期望 true, true, false, false。权限函数是当前状态证据,仍要结合角色成员关系、owner 和 RLS 解释。
v0 给 app 直接表 DML 是教学范围,未来 ch12 可能把部分命令收敛到更窄接口;权限演进必须与应用发布一起设计。
3.4.3 避免依赖不受控的 search_path
search_path 是名称解析规则,也是信任列表。若某角色能在路径靠前的 schema 中创建对象,未限定函数、操作符或关系名可能解析到攻击者对象。
本书运行角色默认:
ALTER ROLE pg36_app IN DATABASE pg36_shop
SET search_path = pg_catalog, shop;迁移脚本每次又显式:
SET search_path = pg_catalog, shop;随后验证 current_schemas(false)。双重设置让正常会话有安全默认,也让脚本不依赖数据库外部配置。
为什么 pg_catalog 放在前面
即使没有写入路径,PostgreSQL 也会隐式搜索 pg_catalog;显式把它放在前面,能防止同名用户对象抢在系统函数/操作符之前解析。临时 schema 对关系与类型还有特殊搜索规则,安全敏感代码仍应写出限定名。
v0 DDL 使用:
pg_catalog.md5(...)
shop.sales_order
shop_api.order_summary不是所有普通查询都必须把每个内置函数写全,但迁移、SECURITY DEFINER 代码和生成 SQL 应采用更严格限定。
不把 $user, public 当无害默认
默认 search path 通常包含 "$user", public。若同名用户 schema 存在,或者 PUBLIC 仍能在 public 创建对象,解析结果可能与预期不同。是否危险取决于权限,但最简单的基线是:
- 撤销不必要的 public CREATE;
- runtime 不拥有路径中的 schema;
- 路径只含受信 namespace;
- 关键对象使用显式限定;
- 每个函数设置安全 search path,不继承调用者环境。
函数安全在 ch13/ch23 展开。
schema 权限分两层
拥有 schema USAGE 才能解析其中对象;仍需相应表/view 权限才能访问。反过来,表上有 SELECT 但无 schema USAGE,也不能通过普通限定名访问。
shop_private 对 app 撤销 USAGE,且其中未来对象不授予 runtime。owner 和超级用户仍可访问,所以它是实现边界,不是对管理员的保密区。
本节验收
- 每个 schema 有内容、消费者与兼容承诺;
- 不把 schema 宣称为独立数据库或租户隔离;
- owner 是 NOLOGIN,runtime 不拥有业务对象;
- 当前权限与 default privileges 分开验证;
- app/ro 不能使用
shop_private; - search path 只含受信 schema,迁移对象显式限定;
- 不把普通 view 当成未经审查的安全屏障。
参考资料
- PostgreSQL 18:模式
- PostgreSQL 18:安全使用模式
- PostgreSQL 18:权限
- PostgreSQL 18:默认权限
- PostgreSQL 18:CREATE VIEW
- PostgreSQL 18:规则与 view 权限
上一节:关系与引用完整性 · 返回本章目录 · 下一节:规范化与有意识的冗余 · 查看全书目录 · 查看索引中心