关于 ld-permissions
面向维护 Lightdash 权限系统的 Agent,说明基于 CASL 的作用域、角色和授权检查流程。内容涵盖新增权限、排查访问问题及作用域变更时的数据迁移。
主要功能
- 01CASL 权限流程说明
- 02作用域与自定义角色指南
- 03前后端授权检查模式
- 04权限问题排查步骤
- 05作用域变更迁移清单
适用场景
- 01为功能添加新权限作用域
- 02排查用户访问被拒问题
- 03修改角色和作用域映射
- 04迁移自定义角色权限数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ lightdash/ lightdash --skill ld-permissions安装后在 Claude Code 中输入/ld-permissions确认已加载。
详细介绍
权限与授权指南
此技能可帮助你使用基于 CASL 的 Lightdash 权限系统,包括作用域、自定义角色和授权实施。
你需要什么帮助?
- 添加新的作用域/权限 - 添加新权限的分步指南
- 排查权限问题 - 排查用户无法访问某项内容的原因
- 了解权限流程 - 了解权限如何端到端地发挥作用
- 使用自定义角色 - 创建或修改具有特定作用域的自定义角色
快速参考
关键文件
| 用途 | 位置 |
|---|---|
| 作用域定义 | packages/common/src/authorization/scopes.ts |
| CASL 类型 | packages/common/src/authorization/types.ts |
| 能力构建器(系统角色与自定义角色路径) | packages/common/src/authorization/index.ts |
| 系统角色能力(项目级) | packages/common/src/authorization/projectMemberAbility.ts |
| 系统角色能力(组织级) | packages/common/src/authorization/organizationMemberAbility.ts |
| 服务账户能力(企业版、CI/CD) | packages/common/src/authorization/serviceAccountAbility.ts |
| 角色到作用域的映射 | packages/common/src/authorization/roleToScopeMapping.ts |
| 作用域到 CASL 的转换 | packages/common/src/authorization/scopeAbilityBuilder.ts |
常见模式
后端权限检查(服务接收 account: RegisteredAccount,并通过 this.createAuditedAbility(account) 构建能力——原始的 user.ability 属于旧模式,参见 docs/account-patterns.md):
import { subject } from '@casl/ability';
import { ForbiddenError } from '@lightdash/common';
const ability = this.createAuditedAbility(account);
if (ability.cannot('manage', subject('Dashboard', { organizationUuid, projectUuid }))) {
throw new ForbiddenError('You do not have permission');
}
CASL 主体范围限定:资源,而非行动者
CASL 行动者会在检查之前传入:
getUserAbilityBuilder({
user: lightdashUser, // actor
projectProfiles,
permissionsConfig,
});
const ability = this.createAuditedAbility(accountOrUser); // actor
subject(...) 必须只描述目标资源:
ability.can(
'manage',
subject('X', {
organizationUuid: target.organizationUuid,
projectUuid: target.projectUuid,
}),
);
切勿使用 user.organizationUuid 等行动者字段来填充 subject(...)。组织级授权可能只检查 organizationUuid,因此,在多组织实例中,来自行动者的主体字段可能导致跨组织访问。单组织开发环境会掩盖这个问题。
前端权限检查:
const { user } = useApp();
if (user.data?.ability.can('manage', 'Dashboard')) {
return <EditButton />;
}
或封装在 CASL 组件中:
import { Can } from '../../providers/Ability';
<Can I="manage" a="Dashboard">
<EditButton />
</Can>
完整文档
如需完整文档,请阅读:.context/PERMISSIONS.md
其中包括:
- 展示完整权限流程的架构图
- 所有作用域组和修饰符(@self、@public、@space 等)
- 自定义角色的数据库架构
- 添加新作用域的分步指南
- 故障排查指南
添加新作用域(快速指南)
你必须更新所有相关的能力层:
-
添加主体(如有需要)到
packages/common/src/authorization/types.ts中的CaslSubjectNames -
在
packages/common/src/authorization/scopes.ts中定义作用域:
{
name: 'manage:NewFeature',
description: 'Description for custom role UI',
isEnterprise: false,
group: ScopeGroup.PROJECT_MANAGEMENT,
getConditions: (context) => [addUuidCondition(context)],
}
-
在
packages/common/src/authorization/projectMemberAbility.ts中更新项目级能力——将其添加到相应的系统角色函数中(例如developer、admin) -
如有需要,在
packages/common/src/authorization/organizationMemberAbility.ts中更新组织级能力——请注意:组织级能力会叠加,且不能受项目级自定义角色限制 -
在
packages/common/src/authorization/roleToScopeMapping.ts的BASE_ROLE_SCOPES中添加到系统角色(必须与projectMemberAbility.ts保持同步——roleToScopeParity.test.ts奇偶性测试会确保这一点) -
在
packages/common/src/authorization/serviceAccountAbility.ts中更新服务账户——如果服务账户需要此权限,则将其添加到ORG_ADMIN(或其他服务账户作用域)。遗漏此项会导致 CI/CD 流水线中断。 -
通过
this.createAuditedAbility(account)+ability.cannot()在服务中实施权限检查——切勿使用原始的user.ability(旧模式,参见docs/account-patterns.md) -
使用
useApp()→user.data?.ability.can()添加前端检查
更改作用域词汇表(迁移自定义角色)
自定义角色以字符串形式将作用域名称保存在 scoped_roles 表中(role_uuid、scope_name、granted_by)。它们与系统角色相互独立,且在作用域词汇表发生变化时不会自动更新。任何重命名、拆分、合并或移除操作都必须包含 Knex 迁移,用于协调现有行;否则,可自托管实例会悄无声息地失去或保留权限。
合并作用域变更前,请评估影响并编写迁移:
| 变更 | 对 scoped_roles 的影响 | 所需迁移 |
|---|---|---|
重命名作用域(例如,manage:Foo → manage:Bar) | 旧行引用了 scopes.ts 中已不存在的名称。parseScopes 会将其视为无效项并丢弃,从而悄无声息地撤销访问权限。 | UPDATE scoped_roles SET scope_name = 'new' WHERE scope_name = 'old' |
将一个作用域拆分为两个(例如,manage:CustomSql → manage:CustomSql + manage:CustomFields) | 拥有原作用域的角色会失去已移至新作用域的功能权限。 | 为每个拥有原作用域的角色补充新作用域(INSERT ... SELECT ... ON CONFLICT DO NOTHING)。参见 20260417111420_grant_custom_fields_to_custom_sql_roles.ts。 |
| 将两个作用域合并为一个 | 仅拥有其中一个被合并作用域的角色可能会获得或失去功能权限。 | 只要存在任一来源作用域,就插入合并后的作用域;然后删除旧行。 |
| 移除作用域 | 行会引用不存在的作用域名称。parseScopes 会悄无声息地丢弃这些行;每次构建权限时,UserModel 都会记录“Custom role(s) for user ... reference scopes not in the runtime vocabulary”警告。 | 删除孤立行。参见 20260519142606_remove_legacy_dashboard_export_scopes.ts。 |
| 收紧现有作用域的条件 | 无需更改行,但操作人员无法察觉行为变化。 | 无需更改表;在 PR 描述中注明。 |
| 添加全新作用域 | 不会影响任何现有行。只需更新 roleToScopeMapping.ts 中的系统角色。 | 无需为自定义角色编写迁移。 |
迁移约定(一般安全迁移规则请参见 packages/backend/src/database/AGENTS.md):
- 使用
try/catch包裹主体,并在失败时记录可恢复的手动修复命令。这些回填属于尽力而为的清理操作——即使失败,也绝不能阻止后续迁移。 - 插入时使用
ON CONFLICT DO NOTHING,因为(role_uuid, scope_name)是自然唯一键。 - 复制作用域时保留源行的
granted_by,使审计历史指向原始授权人,而不是NULL。 - 提供合理的
down()——通常删除up()插入的行。如果变更不可逆(清理旧数据),请说明down()设为空操作的原因。
更改作用域词汇表时的检查清单:
- 确定变更类型(重命名/拆分/合并/移除/添加/收紧)。
- 如果需要迁移,使用
pnpm -F backend create-migration <name>创建,并遵循上述模式。 - 更新
roleToScopeMapping.ts,使系统角色符合新的词汇表,并运行一致性测试。 - 在 PR 描述中说明此项变更,以便审查者确认数据迁移与代码变更相匹配。
调试权限问题
用户遇到“ForbiddenError”时:
- 检查作用域是否存在——该作用域是否定义在
scopes.ts中? - 检查角色分配——用户的角色是否包含此作用域?
- 检查条件——CASL 条件是否与资源匹配?
- 检查企业版标志——是否设置了
isEnterprise: true,但部署并非企业版? - 检查主体名称——在
CaslSubjectNames中是否大小写匹配?
使用 grep 查找权限检查的位置:
grep -r "ability.cannot.*'manage'.*'YourSubject'" packages/backend/src/services/
请描述你想要完成的任务,或让我解释权限系统的任何方面。
仪表板直接授权——边界规则
直接授权允许与用户共享一个仪表板,让其访问该仪表板拥有的图表,而无需成为该仪表板所在空间的成员。这通过 CASL 访问数组强制执行,而不是通过特殊规则实现,并且受直接访问功能标志控制(关闭 → 行为与普通空间检查完全相同)。
规则:直接授权允许执行影响范围仅限于所属仪表板内部的操作。任何读取、移动或复制内容到仪表板之外的操作,都需要实际的空间访问权限。
它取决于一个区分:
- 由仪表板拥有的图表(设置了
saved_queries.dashboard_uuid,space_id为 null)会继承仪表板的授权——共享仪表板即共享其自身的图表。 - 仅仅位于某个空间中的图表(设置了
space_id)仅受空间访问权限控制——共享一个引用该图表的仪表板,不会授予对该图表的任何权限。
原因:授权意味着“你可以在此仪表板内操作”,因此绝不能借此读取图表所在私有空间的内容,也不能将内容移入授权者从未见过的空间。
唯一的关卡是 SpacePermissionService.getDashboardAccessContext(userUuid, { uuid, spaceUuid })——修改任何授权调用位置之前,请先阅读它的文档注释。它会返回空间上下文,并将请求者的授权作为标记为 grantedVia: 'dashboard' 的 access 行附加其中。
添加或更改图表/仪表板访问检查时:
- 在仪表板内进行的自有图表读取/编辑/创建/删除 → 使用图表的所属
dashboardUuid调用getDashboardAccessContext。 - 跨越边界的操作(移入空间、提升、内容即代码、固定、复制到其他仪表板)→ 传入
uuid: null(或使用getSpaceAccessContext),确保授权绝不会生效。expectNoGrantRows测试哨兵会保护这些操作——如果你通过授权处理边界操作,该测试就会失败。 - 绝不要针对授权仪表板所属之外的
spaceUuid解析授权;该辅助函数会断言这两者相匹配。
写入操作与跨越边界操作的区分位于 SavedChartService 中(识别授权的操作:update、createVersion、create、delete、softDelete、duplicate;仅检查空间权限的操作:moveToSpace、updateMultiple 中的移动部分、固定)。另请参阅 packages/backend/src/services/SpaceService/AGENTS.md。