Skip to content
bitzorcas
中EN

Reference

GDPR 存储、租户、安全与权限

解释 ORM 中立 Store、表结构、双租户谓词、权限码偏差、事务、审计与敏感字段边界。

Last updated

GDPR 数据本身也是高敏感数据:请求状态会暴露用户正在行使哪些权利,DetailsJson 可能聚合多个系统的个人信息,拒绝原因也可能包含敏感事实。存储和授权设计必须按高于普通业务列表的标准处理。

1. 项目与依赖

项目责任
BitzOrcas.Platform.Gdpr.ContractsDTO、请求模型、SAR/Erasure SmartEnum
BitzOrcas.Platform.Gdpr.Application路由命令、查询、权限/Feature 目录、IGdprStore
BitzOrcas.Platform.Gdpr.InfrastructureORM 中立 Store、两张 Owner-local 持久化记录
Identity Contracts/InfrastructureIIdentityPersonalDataEraser 与用户行实现

GdprModule 的治理码是 privacy,声明依赖 Authorization 与 Identity。Infrastructure 只引用 Identity Contracts,不引用具体 ORM。

2. 表结构

SysConsent

[BitzTable(... IsTenant = true, IsSoftDelete = true)],索引为 TenantId + UserId + Purpose。没有唯一索引和并发版本业务合同。

SysDataSubjectRequest

同样是租户软删除表,包含:

  • RequestId(36)、UserId(36);
  • RequestType 与共享 StatusCode;
  • 请求、完成和冷静期时间;
  • ExportFileId(36);
  • RejectionReason(500);
  • 无长度上限元数据声明的 DetailsJson。

索引只覆盖 TenantId + UserId + RequestType 与 StatusCode,没有 RequestId 唯一约束或运营队列的 Tenant/Status/Deadline 组合索引。

3. ORM 中立 Store

GdprStore 依赖两个 IEntitySet<T> 和 Identity 擦除端口。Source Generator 根据 [BitzTable] / [BitzColumn] 生成持久化元数据,当前集成测试证明 SqlSugar 与 EF Core 的基础行为一致。

扩展 Store 时保持 ORM 中立
public sealed class GdprStore(
IEntitySet<ConsentHistoryRecord> consents,
IEntitySet<DataSubjectRequestWorkflowRecord> requests,
ICurrentTenant currentTenant)
{
// 只从可信当前租户取数据面,不接受 HTTP Body 中的 TenantId。
public Task<DataSubjectRequestWorkflowRecord?> FindAsync(
string id,
CancellationToken cancellationToken)
{
var tenantId = currentTenant.Tenant.EffectiveTenantId;
// TenantId 必须留在表达式树内,由两个 ORM Adapter 等价翻译。
return requests.FirstOrDefaultAsync(
row => row.Id == id && row.TenantId == tenantId,
cancellationToken);
}
}

不要在模块内注入 SqlSugarClient 或 DbContext;新增查询形状后需扩展双 Adapter 契约测试。

4. Fail-closed 组合

API Core Runtime 通过 PersistenceDefaultPort(typeof(IGdprStore)) 声明默认端口。只有生产持久化前置条件满足并选择 ORM Adapter 后,[RegisterPersistenceAdapter<IGdprStore>] 才提供 GdprStore。

Shell 模式能解析到显式 Unavailable 代理,但这只证明“不静默使用内存假实现”。上线探针还要真正执行一次受控读写或专用 Readiness,确认:

  • 当前 Provider 已注册;
  • 两张表及索引存在;
  • Identity 擦除端口可用;
  • 事务和密钥服务健康;
  • 导出对象存储与通知端口在启用相应能力时可用。

5. 双重租户谓词

列表方法同时检查调用方传入 TenantId 和 Effective Tenant:

当前 Store 的租户交集
row => row.TenantId == requestedTenantId
&& row.TenantId == currentTenant.Tenant.EffectiveTenantId
&& row.UserId == subjectUserId;

这能阻止调用方传入另一个租户,但应用层使用 Actor Tenant 而不是 Effective Tenant。operate-as 时交集为空。Insert 又忽略 NewConsentRecord.TenantId / NewDataSubjectRequestRecord.TenantId,始终写 Effective Tenant。

正确的应用合同应显式区分:

  • ActorTenantId:谁发起操作;
  • EffectiveTenantId:数据归属;
  • SubjectUserId:权利主体;
  • ImpersonationGrantId:代操作依据。

6. 实际权限码

RBAC 评估器按 {module}.{resource}.{action} 小写构造权限码。当前请求与目录映射如下:

场景实际决策码目录中最接近的声明
创建 SAR/Erasuregdpr.own-data.create无
撤回同意/取消擦除gdpr.own-data.delete无
处理 SARprivacy.sar.updateprivacy.sar.manage
执行擦除privacy.erasure.deleteprivacy.erasure.manage
查看本人列表仅认证privacy.consent.view 未使用

在权限码统一并有端到端授权测试之前,不能把目录常量当成可配置权限清单。

7. Feature 不是当前运行时门禁

GdprFeatures.Operations = "privacy.operations" 默认关闭,并被治理生成器收集。但 GDPR Commands 没有显式 Feature 请求合同,源码也没有对该常量的业务引用。

Feature Catalog 的存在证明“模块声明了一个 Feature”,不能证明路由会因 Feature 关闭而拒绝。生产启用需补端到端测试:同一租户开/关时,HTTP、Mediator 和后台执行的行为一致。

8. 事务边界

所有 GDPR 写命令默认经过 TransactionPipelineBehavior:

  • 成功 Result 才提交;
  • 失败 Result 回滚;
  • 系统异常回滚后重抛;
  • Activity 审计在 Handler 返回后尝试入队,但其投递是 best-effort。

这解释了 ProcessSar 的 Failed 状态为什么不会保存。也说明通用 Activity 记录不能作为与业务事务原子提交的合规证据。

9. 敏感数据与日志

DetailsJson、RejectionReason 和导出对象应采用字段级分类。当前 ProcessSar 把 ex.Message 写入 RejectionReason,可能暴露基础设施信息;Audit 数据虽然声明已脱敏,但 Provider 当前只是硬编码 SensitiveFieldsMasked: true。

建议:

  1. 数据库只保存稳定 ErrorCode,诊断详情进入访问受控的安全日志;
  2. Details 使用版本化 Schema 和应用层加密;
  3. 列表 DTO 不返回 Details、下载密钥或内部拒绝说明;
  4. 所有日志使用 RequestId/CorrelationId,不输出导出 Body;
  5. 访问 Details 和下载包使用单独的高权限与审计类别。

10. 数据保留

当前两张 GDPR 表没有模块专属保留任务。软删除不会自动物理清理,DetailsJson 也没有过期字段。

保留设计需要分别定义:

数据典型决策
同意历史按合法证明期限保留,撤回后不立即删除
DSR 工作流元数据保留处理证明,但最小化主体数据
导出明文包短期保留,到期物理删除
失败诊断脱敏、短期、受限访问
Legal Hold暂停清理并记录依据与解除

不要复用审计模块当前存在分类语义缺陷的通用 Retention Port 来清理 GDPR 导出。

11. 索引与并发目标

目标约束示意
-- 请求幂等必须至少限定租户与请求类型。
CREATE UNIQUE INDEX UX_Dsr_Request
ON SysDataSubjectRequest (TenantId, RequestType, RequestId);
-- 运营队列需要按租户、类型、状态和截止时间扫描。
CREATE INDEX IX_Dsr_WorkQueue
ON SysDataSubjectRequest
(TenantId, RequestType, StatusCode, CoolingOffEndsAt);

还需使用聚合 Version 或专门并发令牌执行 Compare-and-Swap,防止两个 Operator 同时推进状态。

12. 审查命令

Terminal window
# 检查模块没有直接依赖具体 ORM。
rg -n "SqlSugar|DbContext|EntityFrameworkCore" src/Platform/Gdpr -g '*.cs'
# 检查实际权限资源与目录。
rg -n "ResourceDescriptor|PermissionDefinition|GdprPermissions" \
src/Platform/Gdpr -g '*.cs'
# 检查敏感详情的所有读写点。
rg -n "DetailsJson|RejectionReason|ExportFileId" src/Platform/Gdpr -g '*.cs'

返回 GDPR 总览

100%

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