Files
story/docs/privacy-requests.md

131 lines
7.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 隐私数据请求开发说明
## 1. 当前交付边界
当前版本完成了`ACCESS`数据访问申请和`DELETE`主体删除申请的端到端闭环。`ACCESS`由Worker在服务端租户/家庭/长者边界内盘点数据范围;`DELETE`在受理事务内立即冻结主体,再由Worker对六个子系统逐项处理、持久化状态并对账,只有全部子任务进入`COMPLETED`或经核验的`SKIPPED`后才将总请求置为`COMPLETED`。
隐私请求结果始终设置`content_included=false`,不把档案、转写、故事或记忆正文复制到请求表、审计记录和普通日志。正文导出由独立私有导出任务实现,见`data-export.md`。当前仍未开放`CORRECTION`和独立的`CONSENT_WITHDRAWAL`批处理请求。
## 2. 权限与安全边界
| 能力 | 家庭所有者 | 管理员 | 编辑者/查看者 |
|---|:---:|:---:|:---:|
| `privacy.read` | 允许 | 允许 | 拒绝 |
| `privacy.manage` | 允许 | 拒绝 | 拒绝 |
服务端从OIDC主体重新解析账号、客户组织、家庭成员和长者归属,不使用浏览器提交的租户条件。创建请求必须提交16至128字符的`Idempotency-Key`;同键同正文返回原请求,同键不同正文返回`IDEMPOTENCY_KEY_CONFLICT`。详情响应使用`Cache-Control: no-store`和强ETag。
`DELETE`还必须满足以下门禁:
- `reason`为1至500字符,但服务端仅持久化SHA-256摘要,不保存原因正文;
- `confirmation`必须与主体当前称呼完全一致,确认值不持久化;
- 主体存在`PREPARING`、`ROOM_ISSUED`、`STARTING_AGENT`、`ACTIVE`或`STOPPING`通话时返回`SUBJECT_ACTIVE_CONVERSATION`,必须先正常结束通话;
- 受理后主体立即变为`deleting`,一般主体、内容、设备和导出接口不再返回该主体;所有有效授权同时撤回,记忆停用并清除可召回向量;
- 删除申请详情使用请求编号仍可查询,即使主体已进入`deleting`或`deleted`。
## 3. 业务流程
```mermaid
sequenceDiagram
participant U as 家庭所有者
participant A as API
participant D as PostgreSQL
participant O as Outbox
participant W as Worker
U->>A: POST /subjects/{id}/privacy-requests
A->>A: 验证OIDC、privacy.manage、长者边界和幂等键
A->>D: 创建SUBMITTED请求与PENDING子任务
A->>O: privacy.access_requested
A-->>U: 202 + 请求编号
W->>D: VERIFYING -> EXECUTING
W->>D: 按冻结分类统计当前长者数据
W->>D: 子任务COMPLETED,请求COMPLETED
W->>D: 写入无正文系统审计
U->>A: GET /privacy-requests/{id}
A-->>U: 状态、分类数量和子任务
```
### 3.1 DELETE删除与对账流程
```mermaid
sequenceDiagram
participant U as 家庭所有者
participant A as API
participant D as PostgreSQL
participant O as Outbox
participant W as Worker
participant R as Redis
participant S as MinIO
U->>A: DELETE申请+原因+精确称呼确认
A->>D: 校验权限、确认值及无活动通话
A->>D: 主体deleting、撤回授权、停用记忆/向量
A->>D: 创建6个PENDING子任务
A->>O: privacy.delete_requested
A-->>U: 202 + 请求编号
W->>R: 删除会话租约和设备在线键
W->>S: 删除所有私有导出对象
W->>D: 核验媒体对象和外部索引引用
W->>D: 清空pgvector及供应商引用
W->>D: 脱敏正文、快照、回调、绑定和档案
W->>D: 六任务对账+最小tombstone
W->>D: 请求COMPLETED或PARTIAL_FAILED
```
六类子任务如下:
| 子系统 | 当前动作 | 成功判定 |
|---|---|---|
| `redis` | 删除该主体历史通话租约键和关联设备在线键 | Redis删除操作成功,允许键本来不存在 |
| `export_files` | 删除全部`export_task.object_ref`对应私有对象,任务改为`EXPIRED`并清空对象元数据 | 每个对象删除成功且数据库引用为空 |
| `object_storage` | 删除该主体全部已登记录音对象,并把分片置为`DELETED`、清空对象引用和文件摘要 | 对象存储删除失败时记`FAILED`并持续重试;无对象时安全完成 |
| `pgvector` | 清空本地embedding和provider引用,索引记`DELETED` | 目标主体本地向量均不可召回 |
| `provider_index` | 检查外部供应商索引引用 | 无外部引用时记`SKIPPED`;存在引用但无删除适配器时失败并使总请求`PARTIAL_FAILED` |
| `postgresql` | 对档案、事实、转写、摘要、故事、记忆、快照、回调和绑定做删除或不可逆脱敏 | 主体为`deleted`,正文为固定删除标记,审计只留最小证明 |
## 4. 接口
| 接口 | 能力 | 关键约束 | 结果 |
|---|---|---|---|
| `POST /api/v1/subjects/{id}/privacy-requests` | `privacy.manage` | 接受`ACCESS`或`DELETE`;必须`Idempotency-Key`;DELETE还需原因和精确确认 | `202`受理,幂等重放为`200` |
| `GET /api/v1/subjects/{id}/privacy-requests` | `privacy.read` | 固定长者边界 | 请求列表及盘点摘要 |
| `GET /api/v1/privacy-requests/{id}` | `privacy.read` | 服务端反查家庭归属;禁止缓存 | 请求详情和子任务 |
可请求的分类为`profile`、`consents`、`device_bindings`、`conversations`、`transcripts`、`summaries`、`stories`和`memories`。不指定分类时盘点全部分类。
## 5. 数据库
| 表 | 作用 | 关键约束 |
|---|---|---|
| `privacy_request` | 隐私请求聚合根 | 租户+申请人+幂等键唯一;请求摘要用于检测正文冲突 |
| `privacy_subtask` | 子系统执行与重试状态 | 请求+子系统+目标唯一;保留尝试次数和最后错误 |
| `tombstone` | 后续删除闭环的最小不可逆证明 | 只保存资源摘要和保留原因,不保存待删正文 |
V9迁移提供了请求、子任务和最小证明模型。当前`ACCESS`和`DELETE`已开放;`CORRECTION`与独立`CONSENT_WITHDRAWAL`仍保持关闭。
独立临时空库已完成`up 1—9 → down -all → up 1—9`,最终迁移状态为`9|dirty=false`,验证库随后已删除。
## 6. 自动化验证
`scripts/smoke-privacy.ps1`使用真实本地OIDC令牌验证:
- 所有者创建、同键重放和同键不同范围冲突;
- 编辑者提交请求返回403;
- Worker完成数据库子任务,请求收敛为`COMPLETED`;
- 返回的分类严格等于请求范围且`content_included=false`;
- 列表可见、测试数据和关联Outbox/审计可清理。
`scripts/smoke-privacy-delete.ps1`使用临时家庭和主体,并验证:
- 错误称呼确认和非所有者提交均被拒绝;
- 存在活动通话时删除返回409,正常结束后才允许受理;
- 受理后普通主体读取和新建导出立即被阻断;
- 冻结后的同键同正文仍能安全重放,不会创建第二个请求;
- 六个子任务全部收敛为`COMPLETED`或经核验的`SKIPPED`;
- 档案和稳定事实正文已脱敏,原因明文未进入请求表;
- 私有导出数据库引用与MinIO对象均已删除;
- 仅留下不含个人正文的SHA-256主体摘要墓碑;
- 测试完成后临时业务数据、Outbox、审计和对象均被清理。
家庭工作台的“提交全量盘点”现通过页面内受控操作区提交。操作区固定当前长者公开编号和八类盘点范围:长者档案、授权记录、设备绑定、通话、话轮、摘要、故事和记忆;同时明确ACCESS结果仅返回各分类数量、处理状态和子任务状态,不会把个人正文复制到隐私请求、审计或普通日志。用户必须显式确认范围后才能提交,取消或切换业务上下文会清除草稿且不会创建请求。
页面按低强度策略使用真实OIDC执行无提交冒烟:检查固定对象、八类范围、正文边界、确认门禁和取消行为,并验证零创建请求、请求数量不变、无横向溢出及无非预期浏览器错误。服务端权限、幂等、租户隔离和Worker闭环继续由既有定向脚本覆盖,不在每次页面改动后重复执行全套隐私删除回归。