Skip to content
bitzorcas
中EN

Guide

GDPR 擦除与归属模块编排

讲清 30 天冷静期、当前 Identity 匿名化范围、状态迁移漏洞、法律保全与生产级归属模块协议。

Last updated

“被遗忘权”不是对所有表执行 Delete。正确实现要区分删除、匿名化、法定保留、不可变财务/安全证据和外部 Processor 通知,并留下可证明的逐步骤结果。

1. 当前三条路由

路由当前行为
POST /api/privacy/erasure创建 CoolingOff,截止时间为当前 UTC + 30 天
DELETE /api/privacy/erasure/{id}本人在 CoolingOff 内取消
POST /api/privacy/erasure/{id}/execute管理员拒绝,或匿名化 Identity 用户

创建和取消使用 gdpr.own-data.create/delete,执行使用 privacy.erasure.delete。这些实际决策码与权限目录的 privacy.erasure.manage 不同。

2. 冷静期

CreateErasure 把新请求直接保存为 CoolingOff(1),并设置 CoolingOffEndsAt = now + 30 days。SmartEnum 中存在 Submitted(0),但创建路径不使用它。

客户端创建后展示可取消截止时间
var response = await http.PostAsJsonAsync(
"/api/privacy/erasure",
new { requestId = stableRequestId },
cancellationToken);
var request = await response.Content
.ReadFromJsonAsync<DataSubjectRequestSummary>(cancellationToken);
// 当前 Summary 不返回 CoolingOffEndsAt。
// 客户端不能仅靠响应精确展示截止时间,需补充详情合同。

当前没有后台任务在 30 天后自动执行或提醒,也没有时区问题,因为计算使用 UTC;但 SLA、逾期升级和人工队列都未实现。

3. 当前状态迁移并未被完整约束

ExecuteErasure 只拒绝 Completed 和 Cancelled;只有当前状态为 CoolingOff 时才检查截止时间。这意味着 Submitted、Executing、Rejected 甚至错误类型请求都可能进入匿名化。

createowner canceloperator supplies reasondeadline elapsedidentity update succeedsCURRENTLY POSSIBLECURRENTLY POSSIBLECURRENTLY POSSIBLE

CoolingOff

Cancelled

Rejected

Executing

Completed

Submitted

状态机必须集中在一个 Transition Guard 中,并以请求类型、期望版本和允许来源状态共同校验。

4. 请求类型混用是高风险缺口

SAR 和 Erasure 共用 SysDataSubjectRequest 与整数 StatusCode。执行、取消只按 ID 获取,没有验证 RequestType:

  • SAR Submitted(0) 被 Erasure 解释为 Submitted,可跳过冷静期直接匿名化用户;
  • SAR Processing(1) 被解释为 CoolingOff,可能被取消;
  • 相同 RequestId 的跨类型创建会命中错误记录。

这是生产阻断项。仅靠前端不显示错误按钮不能构成安全边界。

5. 当前实际擦除范围

GdprStore.AnonymizeUserAsync 先验证请求 TenantId 等于 Effective Tenant,再调用 Identity Owner 的 IIdentityPersonalDataEraser。实现只更新 SysUser:

字段更新
UserName / DisplayNameanon-{digest}
Emailanon-{digest}@erased.local
Normalized fields同步匿名化
Phone空字符串
AvatarUrlnull
PasswordHashERASED
SecurityStamp新 GUID
IsDeletedtrue

Digest 是 SHA256(tenantId:userId) 的前 24 个十六进制字符,没有服务端密钥。它稳定、便于保持唯一性,但对已知 TenantId/UserId 的参与者可重算,不应描述为不可逆匿名化证明。

6. 当前没有被处理的数据

现有端口没有编排:

  • RefreshToken、UserSession、UserToken、ExternalLogin、FIDO2 Credential;
  • PasswordHistory、UserDevice、LoginLog、组织关系;
  • Files、Documents、Comments、Chat、Tickets、Billing 等业务模块;
  • 搜索索引、缓存、备份、数据仓库与第三方 Processor;
  • SysUser 上的 PhoneHash、确认标志、最后登录、OfficeId、OrganizationUnitId、ScimExternalId 等其他字段。

SecurityStamp 更新可能使部分令牌失效,但这不等同于显式撤销所有会话和凭据。软删除也不等于物理销毁或备份过期。

7. 事务语义

执行命令位于 TransactionPipelineBehavior 内:

  1. DSR 状态设为 Executing;
  2. Identity 用户通过同一持久化抽象更新;
  3. DSR 状态设为 Completed;
  4. Result 成功后统一 Commit。

在同一数据库和 UoW Adapter 下,这三组写入可作为一个事务提交。未来加入对象存储、搜索、外部 Processor 后,单库事务无法覆盖所有系统,必须使用持久化 Step、Outbox、幂等消费者和补偿。

8. 法律保全与保留裁决

当前源码没有 Legal Hold 或 Retention Decision。生产协议不能让 GDPR 模块猜测某条订单、发票或安全日志是否可删;数据 Owner 应返回类型化结果。

目标合同:Owner 自主裁决并返回证据
public interface IDataErasureContributor
{
// OwnerCode 在冻结执行清单后不能因部署或重命名而变化。
string OwnerCode { get; }
// Owner 自己裁决删除、匿名化或依法保留,编排器不直接查表。
Task<ErasureOutcome> ExecuteAsync(
VerifiedSubject subject,
ErasureExecutionContext context,
CancellationToken cancellationToken);
}
public sealed record ErasureOutcome(
ErasureDisposition Disposition, // Deleted, Anonymized, Retained
int AffectedRecords,
string EvidenceHash,
string? RetentionReasonCode,
DateTimeOffset? RetainUntil,
bool Retryable);

RetentionReasonCode 应引用受治理的策略目录,而不是保存自由文本法律意见。

9. 推荐编排状态

否是

主体身份再验证

Legal Hold / Retention 预检查

审批与冷静期

冻结 Owner 清单与版本

逐 Owner 执行
幂等键 + Checkpoint

全部必需 Owner
终态?

Partial / Retry / Escalate

CompletedWithEvidence

通知外部 Processor

完成条件必须是冻结清单中每个必需 Owner 都返回终态,而不是“最后一个调用没有抛异常”。

10. 幂等与恢复

每个 Step 的幂等键建议包含 TenantId + ErasureRequestId + OwnerCode + ContractVersion。执行记录至少保存:

  • Attempt、StartedAt、CompletedAt;
  • 输入 Subject 的不可逆引用;
  • Owner ContractVersion;
  • Disposition、AffectedRecords、EvidenceHash;
  • Retryable、NextAttemptAt、LastErrorCode;
  • Operator、Approval、Correlation。

重试不得重新打开 Rejected/Cancelled 请求,也不得绕过新增的 Legal Hold。

11. 测试合同

现有集成测试只证明 Identity 用户在 SqlSugar 和 EF Core 下都会被软删除并生成 @erased.local 邮箱。尚缺:

  1. SAR ID 不能走擦除端点;
  2. Rejected/Executing/Submitted 不得直接执行;
  3. 冷静期边界精确到等号;
  4. 跨租户 ID 不可探测;
  5. 不存在用户时的完成策略;
  6. Identity 关联凭据和会话处理;
  7. Owner 部分失败、重试与 Legal Hold;
  8. 事务提交前后和 Outbox 一致性;
  9. 确定性匿名标识的再识别风险测试;
  10. 备份和下游 Processor 的到期证明。

12. 源码审查

Terminal window
# 查看真实匿名化字段范围。
sed -n '1,180p' \
src/Platform/Identity/BitzOrcas.Identity.Infrastructure/Identity/IdentityPersonalDataEraser.cs
# 证明当前没有请求类型守卫和 Owner 编排。
rg -n "RequestType|IDataErasureContributor|LegalHold" \
src/Platform/Gdpr/BitzOrcas.Platform.Gdpr.Application/Commands
# 找出所有仍引用用户标识的 Identity 记录类型。
rg -n "UserId" src/Platform/Identity -g '*.cs'

返回 GDPR 总览

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%