遥测表 llm_calls 无保留期与访问控制,全部租户正文无限期留存 #12
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
来源
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 做读取隔离。但另外两件事仍是空白:GRANT/REVOKE,也不建议下游怎么分角色。默认状态是「任何能连库的账号都能读全部租户的全文」。提出方的原话是:「无限期保留全部租户全文、且任何能连库的账号都能读」不应是默认状态。
为什么值得单独做
这不是性能问题,是合规面。表里是客户合同与标书全文,多租户混存。保留期缺失意味着删除请求(数据主体权利)无处执行;访问控制缺失意味着 RLS 只挡住了「用错租户上下文查询」,挡不住「用一个有全表权限的账号连上来」。
同时这件事与 #11 的不可逆性论证是同一类:数据一旦以当前形态写进去,事后再想补保留期,已经超期的那部分已经存在了。
可能的方向(未定,需要设计)
created_at做 RANGE 分区、过期靠DROP/DETACH分区实现 O(1) 清理,而非DELETE。pg_partman可设 retention 自动化。这是审计表的通行做法。REVOKE UPDATE, DELETE+ 触发器兜底,本表目前没有任何这类保护。边界提示
按 #11 的结论,库自己执行不可逆的库级 DDL 是危险的(启用 RLS 而无 policy 是 default-deny,会静默击穿非多租户下游)。这条 issue 同样应当先明确「库做什么、下游 DBA 做什么」,再谈实现。
已随 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 权限。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(应用角色)——这是分区方案不可替代的理由。