首页
/ FastGPT 权限继承机制详解:从 ACL 合并到子树同步的完整实现

FastGPT 权限继承机制详解:从 ACL 合并到子树同步的完整实现

2026-09-09 18:14:32作者:农烁颖Land

FastGPT 采用"文件夹 + 普通资源"的树形结构组织知识库、应用、技能等资源,其协作权限支持沿资源树向下继承:子资源开启继承后,鉴权时自动合并父级协作者权限。本文以仓库源码为准,完整讲解 FastGPT 权限继承的模型设计、鉴权合并算法、创建/移动/恢复继承的底层实现与增量同步策略,帮助你在多级文件夹场景下准确预测权限行为并排查授权问题。

1. 继承模型概述

FastGPT 的资源树由两类节点组成:Folder(文件夹)普通资源(如知识库、应用、模型、技能等)。继承关系可以用下面这张图概括(引自 .agents/skills/support/permission/add-permission/reference/inheritance.md 的原始模型):

┌─────────────────────────────────────────────────────────┐
│  Folder A (inheritPermission: false)                    │
│  协作者: [User1: manage, User2: write]                  │
└───────────────────────┬─────────────────────────────────┘
                        │
        ┌───────────────┴───────────────┐
        ▼                               ▼
┌───────────────────┐         ┌───────────────────────────┐
│ Resource B        │         │ Folder C                  │
│ inherit: true     │         │ inherit: true             │
│ 自身协作者: []    │         │ 协作者: [User1, User2]    │
│                   │         │ (从 A 复制)               │
│ 最终权限:         │         └─────────────┬─────────────┘
│ User1: manage     │                       │
│ User2: write      │                       ▼
│ (来自父级)        │         ┌───────────────────────────┐
└───────────────────┘         │ Resource D                │
                              │ inherit: true             │
                              │ 自身协作者: [User3: read] │
                              │                           │
                              │ 最终权限:                 │
                              │ User1: manage (父级)      │
                              │ User2: write (父级)       │
                              │ User3: read (自身)        │
                              └───────────────────────────┘

对应三条关键规则:

  1. Folder 不继承:folder 的 inheritPermission 字段无效(图中 Folder A 虽为 false,但作为根节点它拥有自己完整的协作者列表;即使设为 true 也不会从更上层继承),它始终以自身协作者列表为准;
  2. 普通资源继承:开启继承时,鉴权阶段会将父级权限与自身权限合并(见 dataset/auth.ts 的实现);
  3. 继承是增量合并:不是覆盖。子资源可以在继承父级权限的基础上,拥有额外的显式协作者(图中 Resource D 的 User3: read 即为例证)。

资源 Schema 中的继承字段

继承能力落地在资源 Schema 的 parentIdinheritPermission 两个字段上(参见 core-concepts.md 中对资源 Schema 的说明):

const ResourceSchema = new Schema({
  teamId: { type: Schema.Types.ObjectId, required: true },
  tmbId: { type: Schema.Types.ObjectId, required: true },   // 创建者/owner
  parentId: { type: Schema.Types.ObjectId, default: null }, // 父资源(可选)
  inheritPermission: { type: Boolean, default: true }       // 是否继承(可选)
});
  • parentId 指向父资源(folder 或 null);inheritPermission 默认 true
  • 需要特别注意的是历史数据兼容:在 resourcePermissionPolicy.ts 中,shouldInheritResourcePermission 的实现是 inheritPermission !== false,即缺失该字段的历史资源也会被当作开启继承处理,避免旧数据因字段缺失而丢失权限。

2. 前置基础:位字段(bitmask)权限模型

理解继承合并的前提,是知道权限在 FastGPT 中是一组位字段。核心常量定义在 constant.ts

export const NullRoleVal = 0;                       // 无角色
export const OwnerRoleVal = ~0 >>> 0;               // 所有位为1,表示所有者
export const CommonPerList = {
  owner: OwnerRoleVal,   // 全 1
  read: 0b100,           // 读权限 (4)
  write: 0b010,          // 写权限 (2)
  manage: 0b001          // 管理权限 (1)
};

权限值对照表:

权限 二进制 说明
NullRoleVal 0 0b000 无角色
ReadPermissionVal 4 0b100 读权限
WritePermissionVal 2 0b010 写权限
ManagePermissionVal 1 0b001 管理权限
OwnerPermissionVal ~0>>>0 全1 所有者

关键区分:数据库中 permission 字段存储的是角色值(role value),通过 CommonRolePerMap 展开为实际权限位(见 constant.ts):

export const CommonRolePerMap = new Map([
  [0b100, 0b100],  // read 角色 -> read 权限
  [0b010, 0b110],  // write 角色 -> write + read 权限
  [0b001, 0b111]   // manage 角色 -> manage + write + read 权限
]);

角色蕴含关系:manage (0b001) 包含 write + readwrite (0b010) 包含 read。这决定了继承合并时"manage 覆盖 write"的直觉来源——两者按位或的结果仍是 manage 所代表的全套权限。

3. 鉴权时的权限合并:一次查询、按位合并

继承机制在鉴权链路中的落点,是 authDatasetByTmbId(见 dataset/auth.ts)。该函数是知识库(dataset)类资源的统一鉴权入口,核心逻辑如下:

const isOwner = tmbPer.isOwner || String(dataset.tmbId) === String(tmbId);
const isGetParentClb =
  shouldInheritResourcePermission(dataset.inheritPermission) &&
  dataset.type !== DatasetTypeEnum.folder &&
  !!dataset.parentId;
const [folderPer = 0, myPer = 0] = await Promise.all([
  isGetParentClb
    ? getTmbPermission({
        teamId,
        tmbId,
        resourceId: dataset.parentId!,
        resourceType: PerResourceTypeEnum.dataset
      })
    : 0,
  getTmbPermission({
    teamId,
    tmbId,
    resourceId: datasetId,
    resourceType: PerResourceTypeEnum.dataset
  })
]);

const Per = new DatasetPermission({ role: sumPer(folderPer, myPer), isOwner });

if (!Per.checkPer(per)) {
  return Promise.reject(DatasetErrEnum.unAuthDataset);
}

这段代码体现了继承合并的三个细节:

  1. 只对普通资源取父级权限dataset.type !== DatasetTypeEnum.folder 明确了 Folder 不参与继承合并,与第 1 节规则一呼应;
  2. 并行查询:父级权限与自身权限通过 Promise.all 并行获取,getTmbPermission 内部会依次查询个人(tmbId)权限,缺失时再合并成员组(groupId)与组织(orgId)权限(见 controller.ts);
  3. 按位或合并sumPer(folderPer, myPer) 将父级与自身的权限位做按位或,最后封装为 DatasetPermissioncheckPer 校验。

sumPer 的完整实现位于 utils.ts

export const sumPer = (...per: PermissionValueType[]) => {
  if (per.length === 0) {
    // prevent sum 0 value, to fallback to default value
    return undefined;
  }
  const res = per.reduce((acc, cur) => acc | cur, 0);
  if (res < 0) {
    // overflowed
    return OwnerRoleVal;
  }
  return res;
};

两个细节值得注意:空参数时返回 undefined(触发调用方默认值回退),位溢出(结果为负数,如 owner 位与其他位或运算后符号位翻转)时统一收敛为 OwnerRoleVal,保证极端合并场景下行为稳定。

checkPer 的判定逻辑(见 permission-class.md 中的 Permission 类设计):owner 权限必须精确相等,其余权限采用 (permission & perm) === perm 的位掩码判定。

4. Folder 创建时复制父协作者

在资源树的根节点(folder)创建时,FastGPT 会把父级(若存在)的协作者复制到新 folder 上,并保证创建者成为 owner。历史实现是 createResourceDefaultCollaborators(见 controller.ts),其逻辑骨架如下:

export const createResourceDefaultCollaborators = async ({
  teamId,
  tmbId,
  resourceId,
  resourceType,
  parentId,
  session
}) => {
  // 1. 获取父协作者
  const parentClbs = parentId
    ? await getResourceOwnedClbs({ teamId, resourceId: parentId, resourceType })
    : [];

  // 2. 构建新协作者列表
  const collaborators = [
    ...parentClbs
      .filter((item) => item.tmbId !== tmbId)  // 排除创建者
      .map((clb) => {
        // 父 owner 降级为 manage
        if (clb.permission === OwnerRoleVal) {
          clb.permission = ManageRoleVal;
        }
        return clb;
      }),
    // 创建者成为 owner
    { tmbId, permission: OwnerRoleVal }
  ];

  // 3. 批量插入
  await MongoResourcePermission.insertMany(
    collaborators.map((clb) => ({
      teamId,
      resourceType,
      resourceId,
      ...clb
    })),
    { session }
  );
};

规则非常明确:

  • 父 owner 降级为 manage:owner 是"资源自身"的角色,父级 owner 在子 folder 中只能获得 manage 权限;
  • 创建者成为 owner:无论父级是否存在,创建者都会拿到 OwnerRoleVal
  • 创建者同时出现在父协作者列表时会被过滤(tmbId !== tmbId 排除),避免重复。

在当前版本中,这一能力被重构为 createResourcePermissions(见 resourcePermissionService.ts),底层通过 mergeResourceCollaborators 合并父级快照与创建者的 owner 授权,再整体 replaceResource 写入,语义与上述历史实现保持一致。

5. 子树权限同步:syncChildrenPermission 与增量传播

当 folder 的协作者发生变化时,需要把变化同步到所有开启继承的子树节点。这是整个继承机制中最复杂的部分,也经历了明显的实现演进。

5.1 历史实现(文档原始版本)

文档中给出的 syncChildrenPermission 位于 inheritPermission.ts,核心策略为:

export async function syncChildrenPermission({
  resource,
  folderTypeList,
  resourceType,
  resourceModel,
  session,
  collaborators: latestClbList
}) {
  // 1. 只处理 folder
  if (!folderTypeList.includes(resource.type)) return;

  // 2. 获取所有 inheritPermission: true 的 folder 子树
  const allFolders = await resourceModel.find({
    teamId: resource.teamId,
    inheritPermission: true,
    type: { $in: folderTypeList }
  });

  // 3. BFS 遍历子树
  const queue = [resource._id];
  while (queue.length) {
    const parentId = queue.shift();
    const children = allFolders.filter(f => String(f.parentId) === String(parentId));

    for (const child of children) {
      const childClbs = await getResourceOwnedClbs({ resourceId: child._id, ... });

      for (const latestClb of latestClbList) {
        // 跳过 owner
        if (latestClb.permission === OwnerRoleVal) continue;

        const myClb = childClbs.find(c => sameClb(c, latestClb));

        if (myClb) {
          // 已有则合并(增量)
          await MongoResourcePermission.updateOne(
            { _id: myClb._id },
            { permission: sumPer(myClb.permission, latestClb.permission) },
            { session }
          );
        } else {
          // 没有则新增
          await MongoResourcePermission.create([{
            ...latestClb,
            resourceId: child._id,
            resourceType
          }], { session });
        }
      }

      // 删除不再存在的纯继承协作者
      for (const childClb of childClbs) {
        const inLatest = latestClbList.find(c => sameClb(c, childClb));
        if (!inLatest && childClb.permission === parentClb?.permission) {
          await MongoResourcePermission.deleteOne({ _id: childClb._id }, { session });
        }
      }

      queue.push(child._id);
    }
  }
}

历史实现的关键点:

  • 增量合并:子资源已有该协作者时按位或合并,而不是简单覆盖,保留子资源的显式增量;
  • 只删纯继承:只有权限值与父级完全一致的协作者才会被删除,带增量 bit 的记录不受影响;
  • 跳过 owner:owner 是资源自身的角色,不参与继承同步。

5.2 现代表现:syncResourceTreePermissions 与 old/new 快照

在当前源码中,inheritPermission.tssyncChildrenPermission 已成为兼容入口,真正的工作委托给了 syncResourceTreePermissions(见 resourcePermissionService.ts)。新实现引入了一个关键设计——父级权限更新必须携带 old/new 两份快照

export async function syncChildrenPermission({
  resource, resourceType, resourceModel, session,
  collaborators,
  oldParentCollaborators,   // 旧父级 ACL 快照
  newParentCollaborators    // 新父级 ACL 快照
}) {
  const oldCollaborators = oldParentCollaborators ?? (await getResourceOwnedClbs({ resourceId: resource._id, ... }));
  const newCollaborators = newParentCollaborators ?? collaborators ?? oldCollaborators;
  return syncResourceTreePermissions({
    resource, resourceModel, resourceType,
    oldParentCollaborators: oldCollaborators,
    newParentCollaborators: newCollaborators,
    session
  });
}

配套的权限重算算法位于 resourcePermissionPolicy.ts,其核心思想是"子级相对旧父级的独有 bit 保留,父级撤掉的继承 bit 移除":

const childExtra =
  child?.permission === OwnerRoleVal
    ? OwnerRoleVal
    : child
      ? (child.permission & ~oldParent) >>> 0   // 子级独有的授权位
      : 0;
const permission = sumPer(newParent, childExtra) ?? 0;

这一改动解决了历史实现的一个边界问题:注释中明确说明"新的父级权限更新必须传入 old/new 快照,避免父级删除后无法推导旧继承位"(见 inheritPermission.ts)——如果父级协作者被整体删除,仅凭"最新列表"无法得知哪些子级记录是"纯继承"(应当被清除),哪些是"显式增量"(应当保留)。

新实现还做了性能优化:按层(BFS 按层查询 frontier 的直接子资源)而非一次性加载整个团队资源树,并只对 affectedCollaborators(新旧快照中有权限变化的协作者)下发 patch,避免对未变化的部分做无意义更新。

6. 恢复继承:resumeInheritPermission

当用户为某个已关闭继承的资源重新开启继承时,调用 resumeInheritPermission。其历史实现(见 inheritPermission.ts)流程如下:

export const resumeInheritPermission = async ({
  resource, folderTypeList, resourceType, resourceModel, session
}) => {
  const { teamId, parentId, _id: resourceId } = resource;

  // 1. 获取父协作者
  const parentClbs = parentId
    ? await getResourceOwnedClbs({ teamId, resourceId: parentId, resourceType })
    : [];

  // 2. 获取自身协作者
  const selfClbs = await getResourceOwnedClbs({ teamId, resourceId, resourceType });

  // 3. 合并协作者(父 owner 降为 manage)
  const mergedClbs = mergeCollaboratorList({
    childClbs: selfClbs,
    parentClbs: parentClbs.map((clb) => {
      if (clb.permission === OwnerRoleVal) {
        return { ...clb, permission: ManageRoleVal };
      }
      return clb;
    })
  });

  // 4. 删除旧协作者
  await MongoResourcePermission.deleteMany({ resourceType, resourceId }, { session });

  // 5. 插入合并后的协作者
  await MongoResourcePermission.insertMany(
    mergedClbs.map(clb => ({ teamId, resourceType, resourceId, ...clb })),
    { session }
  );

  // 6. 如果是 folder,同步子树
  if (folderTypeList.includes(resource.type)) {
    await syncChildrenPermission({ resource, folderTypeList, resourceType, resourceModel, session, collaborators: mergedClbs });
  }

  // 7. 设置继承标志
  await resourceModel.updateOne(
    { _id: resourceId },
    { inheritPermission: true },
    { session }
  );
};

合并工具函数 mergeCollaboratorList 的实际实现位于 utils.ts:父级与子级同一协作者按位或合并,父级 owner 统一降级为 manage。

当前源码中,resumeInheritPermission 同样收敛为对 resumeResourcePermissionInheritance 的调用(见 resourcePermissionService.ts),使用 calculateInheritedResourceCollaborators(old 与 new 均为当前父级快照)先重算自身 ACL,再 syncResourceTreePermissions 同步子树,最后把 inheritPermission 置回 true。整个流程通过 mongoSessionRun 包裹,保证"重算 → 替换 → 同步子树 → 置位"的原子性。

7. 资源移动时的权限处理

资源从一个父级移动到另一个父级时,必须撤销旧父级的继承影响、叠加新父级的继承影响。文档给出的调用模式是:

// 移动资源后
if (newParentId !== oldParentId) {
  // 获取新父级协作者
  const newParentClbs = await getResourceOwnedClbs({
    resourceId: newParentId,
    ...
  });

  // 同步子树
  await syncChildrenPermission({
    resource: movedResource,
    folderTypeList,
    resourceType,
    resourceModel,
    session,
    collaborators: newParentClbs
  });
}

当前源码将这一流程封装为 moveResourcePermissions(见 resourcePermissionService.ts),实现更为严谨:

export const moveResourcePermissions = async ({
  resource, newParentId, resourceModel, resourceType, newParentCollaborators, session
}) => {
  const [oldParentCollaborators, oldResourceCollaborators] = await Promise.all([
    resource.parentId
      ? resourcePermissionRepo.findByResource({ teamId: resource.teamId, resourceType, resourceId: String(resource.parentId), session })
      : [],
    resourcePermissionRepo.findByResource({ teamId: resource.teamId, resourceType, resourceId: String(resource._id), session })
  ]);

  // 用 old/new 父级快照 + 子级旧 ACL 重算移动后的 ACL
  const newResourceCollaborators = calculateInheritedResourceCollaborators({
    oldParentCollaborators,      // 旧父级
    newParentCollaborators,      // 新父级
    childCollaborators: oldResourceCollaborators  // 资源自身的旧 ACL
  });

  await resourcePermissionRepo.replaceResource({
    teamId: resource.teamId, resourceType,
    resourceId: String(resource._id),
    collaborators: newResourceCollaborators, session
  });

  await syncResourceTreePermissions({
    resource, resourceModel, resourceType,
    oldParentCollaborators: oldResourceCollaborators,
    newParentCollaborators: newResourceCollaborators,
    session
  });

  return { newParentId, collaborators: newResourceCollaborators };
};

移动的核心收益是:子资源(及其子树)中"来自旧父级的继承 bit"被精确剥离,只保留相对旧父级的独有增量,再与"新父级的继承 bit"合并——这与 syncResourceTreePermissions 的 old/new 快照机制完全同源。

8. 继承与显式授权的冲突检测:checkRoleUpdateConflict

继承机制还有一个隐含约束:当用户显式修改某个继承资源的协作者权限时,如果该改动与父级继承的权限冲突(例如把父级授予某人的 manage 删掉),FastGPT 会自动将该资源从继承链中摘除(inheritPermission 置为 false),保证显式授权不被父级覆盖。这一逻辑体现在 updateResourceCollaborators(见 resourcePermissionService.ts)中:

if (
  parentCollaborators &&
  shouldInheritResourcePermission(resource.inheritPermission) &&
  resource.parentId &&
  checkRoleUpdateConflict({
    parentClbs: parentCollaborators,
    newChildClbs: newCollaborators
  })
) {
  await resourceModel.updateOne({ _id: resource._id }, { inheritPermission: false }, { session });
}

checkRoleUpdateConflict 的实现见 utils.ts:将被修改的协作者与父级协作者做差集比对,只要"被修改的权限位与父级权限有交集,或协作者被删除",即判定冲突、退出继承。

9. 总结:继承机制的行为清单

场景 行为 依据源码
普通资源鉴权 开启继承时,父级权限与自身权限按位或合并后校验 dataset/auth.ts
Folder 鉴权 不使用父级权限,只看自身完整协作者列表 同上 dataset.type !== DatasetTypeEnum.folder
创建 folder 复制父协作者,父 owner 降为 manage,创建者为 owner resourcePermissionService.ts
父级协作者变更 BFS 遍历开启继承的子树,按 old/new 快照增量更新 resourcePermissionService.ts
恢复继承 合并父级 + 自身 ACL(保留子级独有 bit),同步子树后置位 resourcePermissionService.ts
移动资源 剥离旧父级继承 bit,合并新父级继承 bit resourcePermissionService.ts
显式授权冲突 冲突时自动关闭该资源的继承 resourcePermissionService.ts

掌握这五条规则,就能够在多级文件夹场景下精确预判任意成员的最终权限:最终权限 = 显式授权 ∪ 沿途开启继承的所有祖先的协作者(owner 降为 manage)。相关参考文档还包含:核心概念(位字段与角色映射)Permission 类设计鉴权函数说明

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395