# 模式、所有权与对象边界

LLMS 索引： [llms.txt](/llms.txt)

---

关系模型还需要命名与权限边界。PostgreSQL schema 是数据库内的 namespace，也参与权限解析；它不是独立数据库、租户隔离或微服务边界的自动实现。边界成立要靠所有权、`USAGE`、对象权限和受控名称解析共同支持。

## 3.4.1 业务模式、接口模式与内部模式 {#item-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

同一数据库可以同时存在：

```text
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 对象所有者与运行角色分离 {#item-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 脚本先以管理员认证，再：

```sql
SET ROLE pg36_owner;
```

于是新对象直接归 `pg36_owner`，而不是先由个人管理员拥有再批量改 owner。NOLOGIN owner 没有可泄露的直接登录凭据。

### owner 权力不是普通 ACL

对象 owner 隐含拥有改变、删除和授权对象的能力。即使撤销 owner 的普通 `SELECT`，owner 仍能重新 grant。因此安全设计不能把“owner 账号”当作日常应用角色。

超级用户又能绕过绝大多数权限边界。L1 的 `dbuser_dba` 只用于教学管理；生产迁移应采用受审计、短时授权和明确发布流程。

### 显式权限与默认权限

setup 对当前五表执行：

```sql
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。

验证实际边界：

```sql
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` {#item-3-4-3}

`search_path` 是名称解析规则，也是信任列表。若某角色能在路径靠前的 schema 中创建对象，未限定函数、操作符或关系名可能解析到攻击者对象。

本书运行角色默认：

```sql
ALTER ROLE pg36_app IN DATABASE pg36_shop
SET search_path = pg_catalog, shop;
```

迁移脚本每次又显式：

```sql
SET search_path = pg_catalog, shop;
```

随后验证 `current_schemas(false)`。双重设置让正常会话有安全默认，也让脚本不依赖数据库外部配置。

### 为什么 `pg_catalog` 放在前面

即使没有写入路径，PostgreSQL 也会隐式搜索 `pg_catalog`；显式把它放在前面，能防止同名用户对象抢在系统函数/操作符之前解析。临时 schema 对关系与类型还有特殊搜索规则，安全敏感代码仍应写出限定名。

v0 DDL 使用：

```sql
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：模式](https://www.postgresql.org/docs/18/ddl-schemas.html)
- [PostgreSQL 18：安全使用模式](https://www.postgresql.org/docs/18/ddl-schemas.html#DDL-SCHEMAS-PATTERNS)
- [PostgreSQL 18：权限](https://www.postgresql.org/docs/18/ddl-priv.html)
- [PostgreSQL 18：默认权限](https://www.postgresql.org/docs/18/sql-alterdefaultprivileges.html)
- [PostgreSQL 18：CREATE VIEW](https://www.postgresql.org/docs/18/sql-createview.html)
- [PostgreSQL 18：规则与 view 权限](https://www.postgresql.org/docs/18/rules-privileges.html)

---

[上一节：关系与引用完整性](../03/) · [返回本章目录](../) · [下一节：规范化与有意识的冗余](../05/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
