FastGPT 权限继承机制详解:从 ACL 合并到子树同步的完整实现
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 (自身) │
└───────────────────────────┘
对应三条关键规则:
- Folder 不继承:folder 的
inheritPermission字段无效(图中 Folder A 虽为 false,但作为根节点它拥有自己完整的协作者列表;即使设为 true 也不会从更上层继承),它始终以自身协作者列表为准; - 普通资源继承:开启继承时,鉴权阶段会将父级权限与自身权限合并(见 dataset/auth.ts 的实现);
- 继承是增量合并:不是覆盖。子资源可以在继承父级权限的基础上,拥有额外的显式协作者(图中 Resource D 的 User3: read 即为例证)。
资源 Schema 中的继承字段
继承能力落地在资源 Schema 的 parentId 与 inheritPermission 两个字段上(参见 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 + read,write (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);
}
这段代码体现了继承合并的三个细节:
- 只对普通资源取父级权限:
dataset.type !== DatasetTypeEnum.folder明确了 Folder 不参与继承合并,与第 1 节规则一呼应; - 并行查询:父级权限与自身权限通过
Promise.all并行获取,getTmbPermission内部会依次查询个人(tmbId)权限,缺失时再合并成员组(groupId)与组织(orgId)权限(见 controller.ts); - 按位或合并:
sumPer(folderPer, myPer)将父级与自身的权限位做按位或,最后封装为DatasetPermission供checkPer校验。
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.ts 的 syncChildrenPermission 已成为兼容入口,真正的工作委托给了 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 类设计、鉴权函数说明。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00