遥测表 llm_calls 无保留期与访问控制,全部租户正文无限期留存 #12

Closed
opened 2026-08-18 10:58:48 +08:00 by iomgaa · 1 comment
Owner

来源

issue #11 第 4 条。当时提出方明确说这条可以另开、不必与前三条绑在一起,前三条已随 1.2.0 之后的 tenant_id / meta 支持落地(见 CHANGELOG「未发布」段),这条一直没有承接的地方。

现状

llm_calls 存的是完整正文messages 落库前只过 digest_messages,而它只对多模态 part 里的 image_url 做 sha256,纯文本原样透传(middleware/cache.py,写入点 middleware/telemetry.py)。response 同理。Embedding 路径有 200 字符上限(embedding.py),LLM 路径没有。

现在 tenant_id 已经是真实列,多租户下游可以挂 RLS 做读取隔离。但另外两件事仍是空白:

  1. 保留期:没有任何 TTL、归档或清理机制。库只管写入,写进去的行永久留存。
  2. 访问控制的默认状态:库不执行任何 GRANT/REVOKE,也不建议下游怎么分角色。默认状态是「任何能连库的账号都能读全部租户的全文」。

提出方的原话是:「无限期保留全部租户全文、且任何能连库的账号都能读」不应是默认状态。

为什么值得单独做

这不是性能问题,是合规面。表里是客户合同与标书全文,多租户混存。保留期缺失意味着删除请求(数据主体权利)无处执行;访问控制缺失意味着 RLS 只挡住了「用错租户上下文查询」,挡不住「用一个有全表权限的账号连上来」。

同时这件事#11 的不可逆性论证是同一类:数据一旦以当前形态写进去,事后再想补保留期,已经超期的那部分已经存在了。

可能的方向(未定,需要设计)

  • 分区与保留:按 created_at 做 RANGE 分区、过期靠 DROP/DETACH 分区实现 O(1) 清理,而非 DELETEpg_partman 可设 retention 自动化。这是审计表的通行做法。
  • 正文的可选摘要化:给 LLM 路径也提供类似 embedding 的长度上限或摘要开关,让不需要全文的下游可以不存全文。注意这会与「审计链需要原始证据」冲突,是取舍不是改进。
  • 访问控制:库不应代下游执行 DDL(与 #11 对 RLS 的边界结论一致),但可以在文档里给出角色划分模板(写入角色只 INSERT、报表角色只读且受 policy 约束)。
  • 不可变性:审计表通行做法是 REVOKE UPDATE, DELETE + 触发器兜底,本表目前没有任何这类保护。

边界提示

#11 的结论,库自己执行不可逆的库级 DDL 是危险的(启用 RLS 而无 policy 是 default-deny,会静默击穿非多租户下游)。这条 issue 同样应当先明确「库做什么、下游 DBA 做什么」,再谈实现。

## 来源 issue #11 第 4 条。当时提出方明确说这条可以另开、不必与前三条绑在一起,前三条已随 1.2.0 之后的 `tenant_id` / `meta` 支持落地(见 CHANGELOG「未发布」段),这条一直没有承接的地方。 ## 现状 `llm_calls` 存的是**完整正文**:`messages` 落库前只过 `digest_messages`,而它只对多模态 part 里的 `image_url` 做 sha256,纯文本原样透传(`middleware/cache.py`,写入点 `middleware/telemetry.py`)。`response` 同理。Embedding 路径有 200 字符上限(`embedding.py`),LLM 路径没有。 现在 `tenant_id` 已经是真实列,多租户下游可以挂 RLS 做读取隔离。但另外两件事仍是空白: 1. **保留期**:没有任何 TTL、归档或清理机制。库只管写入,写进去的行永久留存。 2. **访问控制的默认状态**:库不执行任何 `GRANT`/`REVOKE`,也不建议下游怎么分角色。默认状态是「任何能连库的账号都能读全部租户的全文」。 提出方的原话是:「无限期保留全部租户全文、且任何能连库的账号都能读」不应是默认状态。 ## 为什么值得单独做 这不是性能问题,是合规面。表里是客户合同与标书全文,多租户混存。保留期缺失意味着删除请求(数据主体权利)无处执行;访问控制缺失意味着 RLS 只挡住了「用错租户上下文查询」,挡不住「用一个有全表权限的账号连上来」。 同时这件事**与 #11 的不可逆性论证是同一类**:数据一旦以当前形态写进去,事后再想补保留期,已经超期的那部分已经存在了。 ## 可能的方向(未定,需要设计) - 分区与保留:按 `created_at` 做 RANGE 分区、过期靠 `DROP`/`DETACH` 分区实现 O(1) 清理,而非 `DELETE`。`pg_partman` 可设 retention 自动化。这是审计表的通行做法。 - 正文的可选摘要化:给 LLM 路径也提供类似 embedding 的长度上限或摘要开关,让不需要全文的下游可以不存全文。注意这会与「审计链需要原始证据」冲突,是取舍不是改进。 - 访问控制:库不应代下游执行 DDL(与 #11 对 RLS 的边界结论一致),但可以在文档里给出角色划分模板(写入角色只 INSERT、报表角色只读且受 policy 约束)。 - 不可变性:审计表通行做法是 `REVOKE UPDATE, DELETE` + 触发器兜底,本表目前没有任何这类保护。 ## 边界提示 按 #11 的结论,库自己执行不可逆的库级 DDL 是危险的(启用 RLS 而无 policy 是 default-deny,会静默击穿非多租户下游)。这条 issue 同样应当先明确「库做什么、下游 DBA 做什么」,再谈实现。
Author
Owner

已随 1.2.3 发布(https://gitea.iomgaa.online/iomgaa/PolyGateway/releases/tag/v1.2.3)

三个子问题分层落点:

  • 正文体量:新增 PGW_TELEMETRY_TEXT_CAP,作用于 messages 的每条文本 content、多模态 part 的 text、responsethinking;按每条文本切而非切整串 JSON(后者会产出非法 JSON,让此后一切按 JSON 解析该列的分析全废)。缺省 None 即不截断——这是刻意取舍:截断后遥测不再是审计证据、也无法复现重放,而既有下游正依赖这一点。所以 issue 那句"无限期保留全部租户全文不应是默认状态"只被解决了一半:默认仍是全文,但下游第一次有了手段。
  • 保留期tools/telemetry_retention.py,默认 dry-run(打印将删行数、时间范围、租户分布),--apply 才动手;PG 侧探测到分区表即以退出码 3 让路给 DETACH/DROP PARTITION。库本体不 import 它,也不因此持有任何 DELETE 权限。
  • 访问控制:README 新增「生产部署 DDL 模板(PostgreSQL)」——三角色、REVOKE UPDATE, DELETE、RANGE 分区、RLS。模板的 SQL 由集成测试从 README 里解析出来在真实 PG 上逐条执行,所以文档不会与可用性漂移。

写这一节时发现并修正了一个既有缺陷:1.2.1 README 给的 RLS 模板把写侧 policy 也绑在 app.tenant_id 这个 GUC 上,但 PostgresRecorder 用一个连接池给所有租户写、从不设该 GUC——照抄过的部署,每条 INSERT 都被 policy 拒绝,而遥测失败方向是静默降级,表现是整表零行、只有 warning。CHANGELOG 首条给了自查方法。

(b) 与 (c) 的权限张力也写清了:既要 REVOKE DELETE 又要清理,就只能走 DROP PARTITION(owner)而非 DELETE(应用角色)——这是分区方案不可替代的理由。

已随 **1.2.3** 发布(https://gitea.iomgaa.online/iomgaa/PolyGateway/releases/tag/v1.2.3)。 三个子问题分层落点: - **正文体量**:新增 `PGW_TELEMETRY_TEXT_CAP`,作用于 messages 的每条文本 content、多模态 part 的 text、`response` 与 `thinking`;按每条文本切而非切整串 JSON(后者会产出非法 JSON,让此后一切按 JSON 解析该列的分析全废)。**缺省 None 即不截断**——这是刻意取舍:截断后遥测不再是审计证据、也无法复现重放,而既有下游正依赖这一点。所以 issue 那句"无限期保留全部租户全文不应是默认状态"只被解决了一半:默认仍是全文,但下游第一次有了手段。 - **保留期**:`tools/telemetry_retention.py`,默认 dry-run(打印将删行数、时间范围、租户分布),`--apply` 才动手;PG 侧探测到分区表即以退出码 3 让路给 DETACH/DROP PARTITION。库本体不 import 它,也不因此持有任何 DELETE 权限。 - **访问控制**:README 新增「生产部署 DDL 模板(PostgreSQL)」——三角色、`REVOKE UPDATE, DELETE`、RANGE 分区、RLS。模板的 SQL 由集成测试**从 README 里解析出来**在真实 PG 上逐条执行,所以文档不会与可用性漂移。 写这一节时发现并修正了一个既有缺陷:1.2.1 README 给的 RLS 模板把写侧 policy 也绑在 `app.tenant_id` 这个 GUC 上,但 `PostgresRecorder` 用一个连接池给所有租户写、从不设该 GUC——照抄过的部署,每条 INSERT 都被 policy 拒绝,而遥测失败方向是静默降级,表现是**整表零行、只有 warning**。CHANGELOG 首条给了自查方法。 (b) 与 (c) 的权限张力也写清了:既要 `REVOKE DELETE` 又要清理,就只能走 `DROP PARTITION`(owner)而非 `DELETE`(应用角色)——这是分区方案不可替代的理由。
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: iomgaa/PolyGateway#12