ToolJet 权限模型全解:3 层 RBAC + 5 条硬规则 + 源码级校验逻辑
【免费下载链接】ToolJetOpen-source foundation of ToolJet AI - the enterprise app generation platform for internal tools, dashboards, business applications, workflows and AI agents. Build visually, from a prompt, or from Claude Code, Codex and Cursor over MCP 🚀项目地址: https://gitcode.com/GitHub_Trending/to/ToolJet
测试账号刚进 Builder 组,就有人拿它把内部应用推上了生产。ToolJet 的权限模型(Role-Based Access Control, RBAC)靠一条固定的判定链来拦这类事故。下面沿一次真实请求的生命周期走一遍。
核心链路:权限判定请求如何走完 6 步校验
一次"Builder 打开某应用的发布页"请求,在后端会依次穿过 6 个关卡,任何一步不通过就短路返回:
- 请求进来:命中带
tjModuleId/tjFeatureId反射元数据的控制器,AbilityGuard.canActivate接管; - 拦截:先查
WorkspaceBanList,组织被停用直接抛 403(errorType: 'WORKSPACE_BANNED');再按 feature 查许可证,缺许可抛 451; - 查所属组:上游 guard 把请求用户的组信息挂到
request.tj_group、request.tj_admin_groups(EE 侧填充,CE 恒为空数组); - 读组级开关:
FeatureAbilityFactory.defineAbilityFor依据userAllPermissions的isAdmin / isBuilder给当前用户装配一组FEATURE_KEY; - 查细粒度资源授权:
ability.can(feature, GroupPermissions, resourceId)判定该资源 ID 是否在授权范围内; - 放行 / 抛错:只要有一个 feature 不满足,
forbiddenFeature命中即抛ForbiddenException(403)。
第 4 步的分支逻辑是整条链的"开关总闸",源码在 ability/index.ts:
const { superAdmin, isAdmin, isBuilder } = userAllPermissions; if (superAdmin || isAdmin) { can([FEATURE_KEY.ADD_GROUP_USER, FEATURE_KEY.CREATE /* …省略 30+ 个 feature */], GroupPermissions); return; } if (!isBuilder) return; const adminGroups: GroupPermissions[] = request?.tj_admin_groups || [];这里有个坑:if (!isBuilder) return;意味着 End-user 在"配置权限"这一侧直接出局,拿不到任何FEATURE_KEY。而 Admin 分支return前一次性放通 30 多个 feature,等于把细粒度授权的增删改查全交给管理员。真正决定"能操作哪些具体应用 / 环境"的,是第 5 步的resourceId——它对应的就是下一节的细粒度授权表。
ToolJet RBAC 配置的分层拆解:决策、存储与执行
决策层:谁有权定规则
规则来源分三类,边界由"能否被运行时改写"划清。下表按操作维度列出可配置性:
| 操作维度 | 系统内置(代码写死) | 管理员可配(运行时) |
|---|---|---|
| Admin 组组级开关 | 18 个布尔位出厂全true | 不可配——建 / 改细粒度授权时直接抛错 |
| Builder 组级开关 | 出厂true(appCreate等 18 位) | 仅自定义组可调,默认组改不动 |
canAccessProduction等 4 个环境位 | 受MULTI_ENVIRONMENT许可证门控 | 持许可证才可放开 |
| End-user 构建级位 | 出厂全false | 任何构建级位 → 400 拒绝 |
canEdit/canView | 互斥约束写死 | 二者只能置其一 |
组级开关的出厂值集中在 constants/index.ts 的DEFAULT_GROUP_PERMISSIONS,Builder 与 Admin 在这一层 18 个位完全等价,差别全部落在资源级(细粒度)授权上。
存储层:数据落在哪几张表
核心是 3 张主表 + 3 张动作子表。permission_groups(group_permissions.entity.ts)挂 18 个布尔位 +organization_id,成员经group_users关联(删组级联删成员)。资源级授权的主记录在granular_permissions(granular_permissions.entity.ts):
@Column({ name: 'group_id' }) groupId: string; @Column({ name: 'type', type: 'enum', enum: ResourceType }) type: ResourceType; // 7 类资源 @Column({ name: 'is_all', nullable: false, default: true }) isAll: boolean;type取自ResourceType,共 7 类(app/data_source/workflow/folder/module/workflow_folder/module_folder)。isAll=true(默认)表示覆盖该类资源全集,isAll=false时经group_apps/group_folders中间表逐个绑定具体 ID。每条granular_permissions一对一挂 3 个动作子表(应用 / 数据源 / 文件夹),全部onDelete: 'CASCADE'。应用级动作在 apps_group_permissions.entity.ts,其中 4 个环境位(canAccessDevelopment/Staging/Production/Released)默认全true,真正的收紧发生在写入校验时。
执行层:错误码怎么结构化返回
校验失败不返回裸字符串,而是带type的结构化体,供前端识别并弹二次确认。以 granular-permissions.util.service.ts 的创建路径为例:
if (endUsers.length) throw new BadRequestException({ message: { error: ERROR_HANDLER.EDITOR_LEVEL_PERMISSIONS_NOT_ALLOWED, data: endUsers.map((user) => user.email), type: 'USER_ROLE_CHANGE_ADD_PERMISSIONS', }, });data里附带组内所有 End-user 的邮箱,管理员据此定位需要调整角色的用户。前端读到type后走角色变更确认交互;而更新路径遇到同类冲突且未携带allowRoleChange时,抛的是MethodNotAllowedException(405,type: 'USER_ROLE_CHANGE'),两种状态码对应"新增拒绝"与"需确认后升级"两个不同交互分支。
文档没写但源码写死的 5 条硬规则
- Admin 组禁止配细粒度权限。
validateGranularPermissionCreateOperation/validateGranularPermissionUpdateOperation里只要group.name === USER_ROLE.ADMIN就抛ADMIN_DEFAULT_GROUP_GRANULAR_PERMISSIONS(granular-permissions.util.service.ts)。意图:防止误操作把管理员降权。 - End-user 构建级位硬拒绝,且按资源类型粒度不同。
validateAppResourcePermissionUpdateOperation中group.name === USER_ROLE.END_USER && (actions.canEdit || isModule)即拒绝——Module 连canView(Build-with)也拒,而应用只拒canEdit。意图:模块永远不会分配给 End-user。 canEdit与canView互斥。更新应用授权时if (actions.canEdit) actions.canView = false; else if (actions.canView) actions.canEdit = false;。意图:编辑权隐含查看权,避免二者同时为真产生歧义。- 多环境能力是许可证门控的。
validateEnvironmentPermissions/validateResourceAction先查MULTI_ENVIRONMENT许可证,未持有时给 End-user 组授予 dev / staging / production 环境会复用 End-user 拒绝逻辑。意图:多环境隔离本身随套餐售卖。 allowRoleChange触发自动升级。更新路径确认是构建级更新、组内存在 End-user 且allowRoleChange=true时,调用roleUtilService.changeEndUserToEditor批量升为 Builder;否则抛 405。意图:把"改组权限 → 弹角色变更确认"的前端交互落在服务端兜底。
按场景选对路径:ToolJet RBAC 配置决策树
- 如果你要给一个跨职能团队隔离资源,建自定义组(
type: 'custom')而非动默认三角色:默认组名与开关都改不动,自定义组才能独立配权限位并自由授权应用 / 文件夹。 - 如果你要让某人能进生产环境调试,先确认组织持有
MULTI_ENVIRONMENT许可证,再放开canAccessProduction;否则请求会在写入阶段被 400 拦下,而非界面置灰。 - 如果你要把 End-user 升级为能编辑,走
allowRoleChange确认流程而不是手动改权限位:直接授予构建级位会被结构化错误打回,data里返回的邮箱名单就是待升级名单。
延伸阅读:细粒度授权的建 / 改 / 校验全在 group-permissions 模块目录,建议从util-services/granular-permissions.util.service.ts入手对照本文硬规则;角色与权限的产品侧语义可看 user-roles 文档,它把默认三角色与自定义组的边界讲得比源码更直白。
【免费下载链接】ToolJetOpen-source foundation of ToolJet AI - the enterprise app generation platform for internal tools, dashboards, business applications, workflows and AI agents. Build visually, from a prompt, or from Claude Code, Codex and Cursor over MCP 🚀项目地址: https://gitcode.com/GitHub_Trending/to/ToolJet
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考