修复公开资产授权一致性与签名期限

公开资产 metadata 和作品授权改为在同一 SpacetimeDB 事务快照中判断
移除连接级公开授权订阅缓存对 ACL 的参与
公开读取签名有效期上限固定为 600 秒并保留 owner 和 admin 行为
同步生成 bindings 并更新后端契约与项目记忆
This commit is contained in:
2026-07-13 20:04:29 +08:00
parent 4fc8beea72
commit 052d656a52
15 changed files with 293 additions and 95 deletions
@@ -4048,10 +4048,10 @@
## 2026-07-13 公开作品资产使用派生精确读授权
- 背景:资产 ACL 严格执行后,已登记为 `private` 的作品封面和正式资产不能再依赖 generated 前缀匿名读取;但公开作品仍需要允许访客读取它实际展示和运行的资产。
- 决策:已登记 `asset_object` 继续保持 `private`,新增匿名派生 view `public_work_asset_read_grant`。view 只从 `Published + visible` 作品(`custom-world` 另要求未删除)正式发布快照中收集实际使用的资产,历史作品随 view 计算自动补齐;`spacetime-client` 该 view 纳入每个池连接长期订阅。
- 决策:已登记 `asset_object` 继续保持 `private`,新增匿名派生 view `public_work_asset_read_grant`。view 只从 `Published + visible` 作品(`custom-world` 另要求未删除)正式发布快照中收集实际使用的资产,历史作品随 view 计算自动补齐;资产读取 procedure 在同一事务快照内组合 `asset_object` 该 view,不从连接长期订阅 cache 判断 ACL
- 授权边界:grant 携带作品 owner,API 只有在它与 `asset_object.owner_user_id` 一致,且 `asset_object_id` 或精确 `object_key` 命中时才允许匿名读取。隐藏、删除或取消发布会使 grant 自动消失;参考图、未选中候选图和 `generationInputs` 明确排除。Custom World 只遍历角色、地标、营地、章节和 opening CG 等已知正式根,不能递归 legacy payload 的未知预览 / 编辑字段。
- 禁止项:不得通过放开 `generated-*` 前缀或批量把历史对象改为 `PublicRead` 修复公开作品,两种方式都会让作品可见性生命周期与资产授权脱节,并重新引入跨账号读取。
- 影响范围:`module-assets` 公开资产授权判定、`spacetime-module` 跨玩法公开资产 view、`spacetime-client` 订阅 cache `api-server` 资产读取 ACL。
- 影响范围:`module-assets` 公开资产授权判定、`spacetime-module` 跨玩法公开资产 view 与权威读取 procedure`spacetime-client` facade `api-server` 资产读取 ACL。
- 权威查询:`asset_object` 不进入 client 长期订阅。API 通过仅 runtime service identity 可调用的 procedure,按主键或 `(bucket, object_key)` 服务端索引读取事务内 metadata;只有位置查询明确返回不存在时才允许进入 legacy curated 前缀兼容,procedure 失败、超时或重复位置一律失败关闭。
- 一致性:grant 通过长期订阅进入 API 本地 cache;隐藏、删除或取消发布提交后,在连接收到 view 更新时撤销。已签发 URL 仍受其短 TTL 约束,因此该能力不是对既有签名的瞬时吊销机制。
- 验证方式:公开可见作品的正式资产可匿名读取;未选候选图、参考图、跨 owner 伪造 key 仍返回不存在;隐藏、删除或取消发布后 grant 随 view 更新撤销,再恢复公开可见时随 view 恢复
- 一致性:隐藏、删除或取消发布提交后,后续读取 procedure 的事务快照立即按新状态判断,不等待任意池连接追上订阅水位。公开派生授权、`PublicRead` 和 legacy 兼容读取签名 URL 的有效期最多 600 秒,因此该能力不是对既有签名的瞬时吊销机制owner / admin 读取保持原有效期口径
- 验证方式:公开可见作品的正式资产可匿名读取;未选候选图、参考图、跨 owner 伪造 key 仍返回不存在;隐藏、删除或取消发布后新的读取请求立即拒绝,再恢复公开可见时新的读取请求立即恢复;超长公开 `expireSeconds` 被截断为 600 秒
@@ -3012,7 +3012,7 @@
- 现象:资产 ACL 收紧后,公开页面读取其他作者作品资产集中返回 `404`;对象在 OSS 中真实存在,但已登记 `asset_object.access_policy = private`
- 原因:“作品公开”不等于“作者账号下所有 generated 对象永久公开”。只按 profile / session 关联也会误公开同会话的未选候选图、参考图或生成输入;批量改 `PublicRead` 则无法随作品隐藏、删除或取消发布自动撤销。
- 处理:已登记对象继续保持 `private`,通过 `public_work_asset_read_grant` 只派生 `Published + visible``custom-world` 还必须未删除)正式发布快照实际使用资产的匿名读授权。API 必须同时校验 grant owner 与资产 owner 一致,以及 `asset_object_id` 或精确 `object_key` 命中;明确排除参考图、未选候选图和 `generationInputs`。Custom World 只能扫描角色、地标、营地、章节和 opening CG 等正式根,不能遍历 legacy payload 的未知根。历史作品交给 view 现算补齐,不做永久 ACL 数据补丁。
- 查询边界:不能从 `asset_object` 订阅 cache miss 推断 metadata 不存在;订阅失败或增量尚未到达会让已登记 private 对象误走 legacy 放行。资产定位必须通过受 runtime service identity 限制的 procedure 按 bucket / key 权威点查,失败时拒绝读取;同时不要在每个池连接订阅复制全量 private 资产表。
- 权威查询边界:不能从 `asset_object` `public_work_asset_read_grant` 的连接级订阅 cache 推断当前 ACL;池连接水位不一致会让刚撤销的 grant 继续签发 URL,也会让刚公开的作品短暂 404。资产定位和公开授权必须通过受 runtime service identity 限制的 procedure 在同一事务快照中计算,失败时拒绝读取;同时不要在每个池连接订阅复制全量 private 资产表。公开派生授权、`PublicRead` 和 legacy 兼容读取的签名 URL 最长 600 秒,owner / admin 不受该公开上限影响。
- Remix 边界:拼图、Custom World 和大鱼现有 Remix 会把源资产引用复制到新 owner,但没有持久化不可伪造的资产来源。不得因此放宽跨 owner grant;源作品隐藏后仍公开的 Remix 资产,需要后续通过 Remix 时复制资产或持久化 provenance 解决。
- 验证:资产 owner 本人仍可读;公开可见作品的正式资产可匿名读;跨 owner、只命中前缀、参考图、未选候选图和 `generationInputs` 仍返回不存在;作品隐藏、删除或取消发布后 grant 消失。
- 关联:`server-rs/crates/spacetime-module/src/public_asset_access.rs``server-rs/crates/spacetime-client/src/assets.rs``server-rs/crates/api-server/src/assets.rs``docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`