Skip to content
bitzorcas
中EN

Guide

Comments 目标资源授权、租户与隐私

分析通用评论权限与目标资源访问的差异、User/EffectiveTenant 分裂、作者身份、审核能力和安全接入模式。

Last updated

通用评论的难点不是判断调用者有没有 comments.comment.create,而是判断其是否有权查看并评论具体的 Document/doc-42 或 Ticket/ticket-9。当前 Comments 只完成前一层。

1. 两个授权问题

当前主体

通用能力
comments.comment.create

目标资源策略
Document/doc-42

允许创建评论

通用权限回答“这个主体是否能使用评论能力”;目标策略回答“这个主体是否能访问该资源、资源状态是否允许评论”。两者缺一不可。

当前 ResourceDescriptor 固定为 (comments, comment),不包含 EntityType/EntityId。拥有 Comments View 权限的主体可以请求任意资源键的线程;拥有 Create 权限的主体可以为任意字符串键写评论,即使资源不存在。

2. Owner 规则没有接入

以 Documents 为例,Document 保存 AllowComments,但 Comments 源码不依赖 Documents,也没有读取这个字段。类似地,Ticket 的可见范围、状态和参与者规则没有进入 HTTP handler。

因此以下旧说法并不成立:

  • “创建评论前会由 owner 模块授权”;
  • “关闭 AllowComments 后无法评论”;
  • “删除目标资源会自动删除/隐藏评论”;
  • “评论通知会在持久化后发送”。

源码没有这些调用、事件或订阅方。

3. 目标资源策略注册表

通用模块不应引用每个 owner 的 Infrastructure。可由 owner 注册窄策略:

目标资源评论策略契约
public interface ICommentSubjectPolicy
{
// 每个 owner 注册一个规范化资源类型,例如 Document 或 Ticket。
string EntityType { get; }
// 返回 owner 权威决策;Comments 不直接读取 owner 的持久化模型。
Task<Result<CommentSubjectDecision>> AuthorizeAsync(
string entityId,
CommentOperation operation,
CurrentUser caller,
CancellationToken cancellationToken);
}
public sealed record CommentSubjectDecision(
string CanonicalEntityType,
string CanonicalEntityId,
bool Exists,
bool AllowComments,
string? VisibilityVersion);
Documents owner 的策略实现
public async Task<Result<CommentSubjectDecision>> AuthorizeAsync(
string documentId,
CommentOperation operation,
CurrentUser caller,
CancellationToken cancellationToken)
{
// 先在有效租户内读取权威文档,不信任请求中的 TenantId。
var document = await documents.FindAsync(
caller.EffectiveTenantId, documentId, cancellationToken);
if (document is null)
return Result.Failure<CommentSubjectDecision>(DocsErrors.Document.NotFound);
// 复用 Documents 的资源可见性,而不是只看 Comments 通用权限。
var access = await documentAccess.AuthorizeAsync(
document, Map(operation), caller, cancellationToken);
if (access.IsFailure)
return Result.Failure<CommentSubjectDecision>(access.Error);
// Create/Update 可额外要求 AllowComments;只读和版主路径可有明确例外。
if (operation == CommentOperation.Create && !document.AllowComments)
return Result.Failure<CommentSubjectDecision>(CommentErrors.Disabled);
return new CommentSubjectDecision("Document", document.Id, true, document.AllowComments, null);
}

策略失败必须失败关闭。未知 EntityType 不应退化成“通用权限通过即可”。注册表还应规范大小写和别名,避免 document、Document、docs.document 形成三个孤立线程。

4. 当前租户来源分裂

Comments 同时使用两种租户来源:

路径handler 传入store 强制
Create 幂等/父查询currentUser.User.TenantId参数 TenantId 且等于 EffectiveTenantId
Create InsertNewCommentRecord 带 User.TenantId忽略该值,写 EffectiveTenantId
GetCommentsUser.TenantId参数 TenantId 且等于 EffectiveTenantId
Update/Delete GetById无 tenant 参数EffectiveTenantId

普通请求中两者相同时看不出问题。租户模拟、平台运营或后台任务中二者不同会产生:

  • Create 查不到有效租户中的已有 CommentId,幂等失效;
  • 父评论查不到,无法回复;
  • 插入却写入 EffectiveTenantId,可能触发唯一冲突;
  • GetComments 返回空线程;
  • Update/Delete 反而能按 EffectiveTenantId 找到记录。

所有路径应统一使用一个不可变 EffectiveTenant 快照,并把目标 owner 策略也放在同一租户上下文中。

5. AuthorId 的信任边界

Create 不调用 RequireUserId:

当前作者派生语义
var user = currentUser.User;
// User 调用者保存稳定 UserId;Client/缺失用户会退化成字符串 "0"。
var authorId = user.UserId?.ToString() ?? "0";
// 后续编辑/删除仍用当前 UserId 比较,AuthorId="0" 的记录没有普通作者可认领。
var isAuthor = record.AuthorId == user.UserId?.ToString();

如果 Comments 只支持人类作者,应在创建时 RequireUserId。若支持服务账号,应把 AuthorType + AuthorId 一起保存,不能让所有非 User 调用者共享 "0"。

6. Admin 角色绕过

Update/Delete 用 Roles.Contains("admin", OrdinalIgnoreCase) 判定管理员。这与权限目录无关,也没有模块/租户/资源范围。风险包括:

  • role 命名迁移会静默改变能力;
  • 任意租户的同名 admin 都获得 Comments 全局版主语义(仍受有效租户限制);
  • 无法单独授予 moderator;
  • 审计无法区分作者操作和管理操作;
  • ABAC/功能策略不能参与。

建议新增 comments.comment.moderate 权限或 ICommentModerationPolicy,并记录 Operator、Reason、目标资源和原作者。

7. 读取授权必须下推

当前 GetComments 在通过 Comments View 后直接按自由资源键查询。目标策略至少应先确认资源可见。批量场景或跨资源活动流不能先加载所有评论再内存过滤,必须将 owner 的可见资源集合/数据范围下推,或由 owner 先提供已授权 subject 列表。

Comment storeOwner read modelSubject policyComments APIClientComment storeOwner read modelSubject policyComments APIClientGET subject threadauthorize Viewread subject in effective tenantvisible + versionallowquery effective tenant + canonical subjectpaged thread

8. 隐私与保留

评论正文可能包含个人信息、商业秘密和附件链接。当前模块没有:

  • 敏感内容分类或掩码;
  • 作者导出/擦除贡献者;
  • 法律保留/审核冻结;
  • 目标资源删除后的保留策略;
  • 通知、搜索、审计副本的清理协议。

软删除清空当前表 Body 是一项有益最小化措施,但不等于所有副本擦除。生产设计应让 Comments 参与 GDPR SAR/Erasure 编排,并由 owner 指定资源删除时 cascade、retain-placeholder 或 legal-hold。

9. 内容安全

Comments 保存原始字符串,不解析提及或 Markdown。输入限制只防极长正文,不防 XSS、恶意链接、垃圾内容或 Unicode 欺骗。推荐:

  • 存储原始纯文本/Markdown,与渲染产物分离;
  • UI 使用安全 Markdown renderer 和 URL scheme allowlist;
  • 审核/反垃圾在写入前返回类型化决定;
  • 日志只记录 CommentId/subject 哈希,不记录 Body;
  • 通知摘要做长度限制和输出编码;
  • 附件只引用 Finalized FileId,不接受任意 HTML URL。

10. 安全测试矩阵

场景预期
有 Comments Create、无目标资源访问Forbidden/NotFound
目标不存在NotFound,不创建孤儿线程
Document.AllowComments=falseCreate 拒绝
未登记 EntityTypeValidation/NotSupported
User tenant ≠ Effective tenant所有读写以同一 Effective tenant 决策
Client caller明确拒绝或保存 AuthorType=Client
普通作者超时编辑Forbidden
moderator 删除需要专用权限、原因和审计
owner ACL 撤销后续 GET/写入立即拒绝
HTML/危险 URL渲染安全,存储/日志不执行

返回 Comments 总览

100%

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