数据主体访问请求(SAR)应回答“平台持有哪些与我有关的数据、来源、用途和接收方是什么,并如何安全交付”。当前实现是一条可演示的最小路径,还没有达到可交付导出包的完整度。
1. 路由与状态
| 路由 | 调用者 | 行为 |
|---|---|---|
POST /api/privacy/sar | 已认证用户 + gdpr.own-data.create | 按 RequestId 创建 Submitted |
GET /api/privacy/sar | 已认证用户 | 返回本人 SAR 列表 |
POST /api/privacy/sar/{id}/process | 拥有 privacy.sar.update | 拒绝或同步处理 |
GdprSarStatus 包含 Submitted(0)、Processing(1)、Completed(2)、Failed(3) 和 Rejected(4)。
2. 创建与幂等
CreateSar 只校验 RequestId 非空,然后按 TenantId + RequestId 查询;命中就返回旧记录,否则插入。
var idempotencyKey = Guid.NewGuid().ToString("N");
using var response = await http.PostAsJsonAsync( "/api/privacy/sar", new { // 网络重试必须复用这个值,而不是每次重新生成。 requestId = idempotencyKey }, cancellationToken);
var summary = await response.Content .ReadFromJsonAsync<DataSubjectRequestSummary>(cancellationToken);
// 业务数据库的唯一约束仍是并发幂等的最终防线。当前表没有 TenantId + RequestId 唯一索引,两个并发请求仍可能同时通过查询并插入。查询也不包含 RequestType;如果相同 RequestId 已被 Erasure 使用,CreateSar 会返回那条 Erasure 记录。
3. 处理路径
处理是同步 HTTP Command,使用 StandardCommand 超时策略。没有后台队列、截止时间扫描、恢复游标或进度端点。
4. 当前导出内容
SarExportData 只有四个字段:
UserId;ExportedAt;Profile:实际只是{ UserId }的 JSON;AuditRecords:AuditPage Items 再序列化成字符串。
处理器固定请求 PageIndex: 1, PageSize: 1000,没有继续翻页。审计查询本身还存在多 Provider 全局分页和类别映射差异,详见 审计存储与查询。
DetailsJson 最终是“外层对象 + 两个 JSON 字符串”,调用者需要二次反序列化。它不是稳定、版本化的导出 Manifest。
5. 当前没有导出交付
虽然模型包含 ExportFileId,业务源码没有给它赋值。没有实现:
- 调用
IFileStorage或 Files 模块生成包; - 下载端点、一次性授权或下载次数限制;
- 文件加密、密钥封装、校验和或病毒扫描;
- 包过期与物理清理;
- 导出完成通知;
- 把
DetailsJson返回给主体的详情端点。
因此 Completed 当前只表示 JSON 已写进工作流表,不表示数据主体已经获得可访问副本。
6. 失败状态与事务
ProcessSar 捕获非取消异常,设置 Failed 和 RejectionReason = ex.Message,然后返回 Result.Failure。框架事务管道对失败 Result 执行回滚,所以这次 Failed 更新也会回滚。
原始 ex.Message 还可能包含连接、查询或数据细节。正确做法是提交脱敏失败证据和可重试状态,向 API 只返回稳定错误码。
7. 请求类型混用
GetDsrByIdAsync 只按 ID 和租户查询。ProcessSar 没有检查 entity.RequestType == SarRequestType。Erasure 的状态值 1 会被 SAR 解释为 Processing,随后可能被处理成 Completed 并写入 SAR Details。
if (request.RequestType != GdprRequestType.Sar){ // 不泄露另一类请求是否存在,返回稳定的类型不匹配错误。 return Result.Failure<SarSummary>( GdprErrors.RequestTypeMismatch);}
var transition = SarTransitions.TryStart(request.Status);// 迁移守卫同时检查来源状态和并发版本。if (transition.IsFailure) return Result.Failure<SarSummary>(transition.Error);以上为目标实现形状。当前源码没有 GdprRequestType 和集中状态迁移守卫。
8. 生产级收集器协议
每个数据归属模块应通过窄合同提供数据,而不是由 GDPR 直接查询对方数据库。
public interface IDataSubjectExportContributor{ // OwnerCode 是 Manifest、指标和幂等键中的稳定所有者代码。 string OwnerCode { get; } int SchemaVersion { get; }
// Cursor 由 Owner 解释;编排器只负责原样持久化和回传。 Task<ExportContribution> CollectAsync( VerifiedSubject subject, ExportCursor? cursor, CancellationToken cancellationToken);}
public sealed record ExportContribution( IReadOnlyList<ExportArtifact> Artifacts, ExportCursor? NextCursor, string ContentHash, bool Completed, IReadOnlyList<RetentionNotice> OmittedData);协议必须支持稳定游标、大小限制、版本、哈希、合法保留说明和重复执行。Contributor 只返回本模块拥有的数据。
9. 推荐导出流水线
Manifest 应明确列出已收集 Owner、Schema 版本、文件哈希、被合法排除的数据、生成时间和完成状态;不能只凭一个 Completed 枚举判断完整。
10. 下载安全
生产交付至少需要:
- 下载前进行近期身份再验证或 Step-up MFA;
- 下载票据绑定 Tenant、Subject、FileId、过期时间和一次性消费;
- 对象存储私有,不能把永久 URL 写入日志或通知;
- 响应使用正确 Content-Type、Content-Disposition 和禁止缓存 Header;
- 文件内容和元数据经过敏感数据扫描;
- 下载与销毁都产生耐久证据。
11. 测试与审查命令
当前测试只验证 SAR 列表 Handler 和 Store 的双 ORM 读写,不覆盖 Create/Process、分页、文件或失败事务。
# 证明 SAR 当前只读取审计第一页。rg -n "PageIndex: 1|PageSize: 1000" src/Platform/Gdpr -g '*.cs'
# 证明没有文件写入或下载端点。rg -n "IFileStorage|ExportFileId =|download|Download" src/Platform/Gdpr -g '*.cs'
# 查找请求类型校验;修复前预期处理器中没有对应判断。rg -n "RequestType.*SarRequestType" src/Platform/Gdpr/BitzOrcas.Platform.Gdpr.Application/Commands新增测试必须覆盖并发幂等、跨类型 RequestId、错误 ID 类型、超过 1000 条数据、Provider 分页差异、失败状态持久化、包完整性、票据重放和过期销毁。