Skip to content
bitzorcas
中EN

Guide

Documents 知识库、分类、成员与访问规则

讲解知识库和分类树、成员 JSON、文档 ACL、权限目录、租户过滤、引用完整性与安全落地方式。

Last updated

知识库、分类、成员和访问规则共同描述“内容如何组织、理论上谁能访问”。当前实现已经能保存这些信息,却还没有把成员与 ACL 接入准入决策。本章把数据模型与真正生效的安全控制分开讲。

1. 三种层级

知识库根节点

子知识库

分类根节点

子分类

父文档

子文档

三棵树互不替代:KnowledgeBase.ParentId 组织知识空间,DocumentCategory.ParentId 组织某个知识库内的导航分类,Document.ParentId 组织文档层级。数据库不会自动证明所有标识引用有效。

2. 创建知识库

创建私有知识库记录
POST /api/v1/knowledgebases HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9
Content-Type: application/json
{
"name": "售后运营",
"description": "客服与运营共同维护",
"type": "KnowledgeBase",
"icon": "headset",
"color": "#2864DC",
"parentId": null,
"sortOrder": 10,
"isPublic": false,
"defaultAccessLevel": "View"
}

调用者必须是 User;TenantId 与 CreateBy 从当前身份派生。请求中的 DefaultAccessLevel 默认值在 C# 契约中是 null!,省略字段并不安全,应始终显式传入已登记的 AccessLevel。

当前创建逻辑不检查:

  • 同租户重名;
  • ParentId 是否存在或属于同一租户;
  • 父子是否形成环;
  • IsPublic 与 DefaultAccessLevel 的组合是否合法。

知识库树递归遍历 ParentId,没有 visited 集。根外的环可能从结果中消失;可达环会递归不终止。导入或管理员修改层级前必须先做环检测。

3. 知识库成员是聚合数据

成员通过 MembersJson 与知识库同行保存。Add/Remove 会保持 MemberCount 与集合数量一致,并拒绝重复成员或不存在的移除。

应用服务添加知识库成员
// 成员标识和级别进入知识库的 MembersJson 聚合集合。
using var response = await api.PostAsJsonAsync(
"/api/v1/knowledgebases/kb-support/members",
new
{
userId = "user-2048",
accessLevel = "Edit"
},
cancellationToken);
// 这只证明成员记录已经保存,不证明读取/编辑处理器会检查该成员。
response.EnsureSuccessStatusCode();

SetKnowledgeBaseAccessCommand 可以修改 IsPublic 和 DefaultAccessLevel,但没有 [GenerateEndpoint],因此普通 HTTP 客户端不能直接调用。更重要的是,详情、列表、树和文档查询没有根据成员、公开性或默认级别过滤。

4. 分类的唯一性与引用边界

分类表对 (TenantId, KnowledgeBaseId, ParentId, Name) 建立唯一索引。Create handler 还会先查询重复名称;数据库索引负责并发竞争的最终防线。

Category repositoryCreateCategory handlerClientCategory repositoryCreateCategory handlerClientalt[ParentId exists]unique index closes concurrent raceKB + Parent + Namefind parent in current tenantparentcompare parent.KB with request.KBexists same sibling name?save category

分类创建不验证 KnowledgeBaseId 本身存在。删除分类只检查 DocumentCount > 0,不检查子分类;而 Create/Delete Document 当前不维护该计数。因此它不能可靠阻止删除仍被文档引用的分类。

5. 文档访问规则

DocumentAccessRule 用 PrincipalId、PrincipalType 和 AccessLevel 表达规则。同一 PrincipalId + PrincipalType 只能有一条;创建者以 User 身份调用 HasAccess 时拥有所有级别,否则按 AccessLevel 数值比较。

聚合层访问判断的真实语义
// 这是聚合方法的使用示例。当前 HTTP handlers 尚未调用 HasAccess。
var canEdit = document.HasAccess(
principalId: currentUserId,
principalType: PrincipalType.User,
requiredLevel: AccessLevel.Edit);
// 策略接入后,拒绝结果应由统一错误映射生成 Problem Details。
if (!canEdit)
return Result.Failure(DocsErrors.Document.AccessDenied);

当前源码中 HasAccess 没有业务调用点。这意味着添加规则会改变 DTO/数据库,却不会改变详情、列表、版本、Diff、更新、发布或删除的可达性。

6. 四层安全模型

认证
谁在调用

Tenant predicate
能看到哪个租户

IAuthorizedRequest
资源动作权限

成员 / ACL / 状态
资源级规则

当前实现覆盖不均匀:

路径认证Tenant通用权限成员/ACL
文档写命令是User.TenantId是否
文档 GET/版本/Diff是User.TenantId否否
知识库写命令是User.TenantId是否
知识库 GET/list/tree是User.TenantId否否
分类写命令是User.TenantId是,但目录缺权限否
分类 tree是User.TenantId否否
白板同类路径是User.TenantId写有、读无;目录缺权限否

权限目录声明 document 的 view/create/update/delete/publish/manage,以及 knowledgebase 的 view/create/manage/delete。Category 和 Whiteboard 只有资源常量,没有 [PermissionDefinition]。同时 Publish、标签和 ACL 命令都使用 Update 动作,使 publish/manage 常量没有进入这些通用决策。

7. 正确补齐资源授权

一个可复用的策略端口应同时处理知识库继承、公开性、成员、文档规则和状态,并用于所有读写路径:

目标资源访问策略契约
public interface IDocumentAccessPolicy
{
// 所有 detail/list/version/search/hub 路径复用同一操作语义。
Task<Result> AuthorizeAsync(
DocumentResourceSnapshot resource,
DocumentOperation operation,
CurrentUser caller,
CancellationToken cancellationToken);
}
// 快照只携带决策所需事实,不暴露可变聚合给策略实现。
public sealed record DocumentResourceSnapshot(
string TenantId,
string DocumentId,
string KnowledgeBaseId,
string? CreatorId,
bool KnowledgeBaseIsPublic,
AccessLevel KnowledgeBaseDefault,
IReadOnlyList<KnowledgeBaseMember> Members,
IReadOnlyList<DocumentAccessRule> Rules);

目标实现要遵守:

  1. Tenant 不匹配先失败,不允许 ACL 绕过租户;
  2. 平台管理员绕过必须有显式、可审计能力,不靠字符串角色;
  3. deny/allow 合并顺序固定,并有表驱动测试;
  4. 列表查询必须把可见性下推到存储,不先读全量再内存过滤;
  5. Detail、Version、Diff、下载、搜索、Hub 共享同一策略语义;
  6. 成员/ACL 变更后使相关缓存和实时会话失效。

8. 引用完整性的应用服务示例

创建文档前验证组织关系
public async Task<Result<Document>> CreateDocumentAsync(
CreateDocument request,
CurrentUser caller,
CancellationToken cancellationToken)
{
// 1. 所有父资源都必须在可信租户内读取。
var knowledgeBase = await knowledgeBases.FindAsync(
caller.TenantId, request.KnowledgeBaseId, cancellationToken);
if (knowledgeBase is null)
return Result.Failure(DocsErrors.KnowledgeBase.NotFound);
// 2. 分类不仅要存在,还必须属于目标知识库。
if (request.CategoryId is not null)
{
var category = await categories.FindAsync(
caller.TenantId, request.CategoryId, cancellationToken);
if (category is null || category.KnowledgeBaseId != knowledgeBase.Id)
return Result.Failure(DocsErrors.Category.ParentKnowledgeBaseMismatch);
}
// 3. 写入前执行资源策略;不要把成员数据只当 DTO 展示字段。
var allowed = await access.AuthorizeCreateAsync(
knowledgeBase, caller, cancellationToken);
if (allowed.IsFailure) return Result.Failure<Document>(allowed.Error);
return Document.Create(/* validated ids and server-derived tenant */);
}

这是面向当前缺口的目标实现示例,不是现有 handler 的逐字复制。它说明应补的安全顺序,而不是声称框架已经完成这些检查。

9. 计数与删除

删除用例当前保护风险
KnowledgeBase查询是否存在文档不检查子知识库、分类、白板;软删除
CategoryDocumentCount > 0计数未维护,不检查子分类
Document直接软删除不维护 KB/Category 计数,不发布已审计到的集成事件
Whiteboard直接软删除不检查成员或关联 KB 状态

计数只能作为展示/加速字段,删除前必须查询权威引用。若要事务内维护计数,应让文档与父资源更新落在明确的一致性边界,或使用可重放事件 + 对账任务;不能半维护。

10. 测试矩阵

  • 每个 GET、版本、Diff、搜索和 Hub 的未授权主体;
  • 同租户非成员、跨租户成员、创建者、角色/用户 PrincipalType;
  • ACL 撤销后缓存与活跃连接;
  • 分类跨知识库父节点、并发同名、孤儿 ParentId 和环;
  • 删除含文档/子分类/子知识库/白板的知识库;
  • User.TenantId 与 EffectiveTenantId 不同的模拟/后台场景;
  • permission catalog 与每个 IAuthorizedRequest 派生权限的一致性。

返回 Documents 总览

100%

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