mcpField
L
ld-permissions

ld-permissions

github.com/lightdash安全6.2k398 次安装今天更新
代码仓库

关于 ld-permissions

面向维护 Lightdash 权限系统的 Agent,说明基于 CASL 的作用域、角色和授权检查流程。内容涵盖新增权限、排查访问问题及作用域变更时的数据迁移。

主要功能

  1. 01CASL 权限流程说明
  2. 02作用域与自定义角色指南
  3. 03前后端授权检查模式
  4. 04权限问题排查步骤
  5. 05作用域变更迁移清单

适用场景

  1. 01为功能添加新权限作用域
  2. 02排查用户访问被拒问题
  3. 03修改角色和作用域映射
  4. 04迁移自定义角色权限数据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/lightdash/lightdash --skill ld-permissions

安装后在 Claude Code 中输入/ld-permissions确认已加载。

详细介绍

权限与授权指南

此技能可帮助你使用基于 CASL 的 Lightdash 权限系统,包括作用域、自定义角色和授权实施。

你需要什么帮助?

  1. 添加新的作用域/权限 - 添加新权限的分步指南
  2. 排查权限问题 - 排查用户无法访问某项内容的原因
  3. 了解权限流程 - 了解权限如何端到端地发挥作用
  4. 使用自定义角色 - 创建或修改具有特定作用域的自定义角色

快速参考

关键文件
用途位置
作用域定义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 等)
  • 自定义角色的数据库架构
  • 添加新作用域的分步指南
  • 故障排查指南

添加新作用域(快速指南)

你必须更新所有相关的能力层:

  1. 添加主体(如有需要)到 packages/common/src/authorization/types.ts 中的 CaslSubjectNames

  2. 在 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)],
}
  1. 在 packages/common/src/authorization/projectMemberAbility.ts 中更新项目级能力——将其添加到相应的系统角色函数中(例如 developer、admin)

  2. 如有需要,在 packages/common/src/authorization/organizationMemberAbility.ts 中更新组织级能力——请注意:组织级能力会叠加,且不能受项目级自定义角色限制

  3. 在 packages/common/src/authorization/roleToScopeMapping.ts 的 BASE_ROLE_SCOPES 中添加到系统角色(必须与 projectMemberAbility.ts 保持同步——roleToScopeParity.test.ts 奇偶性测试会确保这一点)

  4. 在 packages/common/src/authorization/serviceAccountAbility.ts 中更新服务账户——如果服务账户需要此权限,则将其添加到 ORG_ADMIN(或其他服务账户作用域)。遗漏此项会导致 CI/CD 流水线中断。

  5. 通过 this.createAuditedAbility(account) + ability.cannot() 在服务中实施权限检查——切勿使用原始的 user.ability(旧模式,参见 docs/account-patterns.md)

  6. 使用 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() 设为空操作的原因。

更改作用域词汇表时的检查清单:

  1. 确定变更类型(重命名/拆分/合并/移除/添加/收紧)。
  2. 如果需要迁移,使用 pnpm -F backend create-migration <name> 创建,并遵循上述模式。
  3. 更新 roleToScopeMapping.ts,使系统角色符合新的词汇表,并运行一致性测试。
  4. 在 PR 描述中说明此项变更,以便审查者确认数据迁移与代码变更相匹配。

调试权限问题

用户遇到“ForbiddenError”时:

  1. 检查作用域是否存在——该作用域是否定义在 scopes.ts 中?
  2. 检查角色分配——用户的角色是否包含此作用域?
  3. 检查条件——CASL 条件是否与资源匹配?
  4. 检查企业版标志——是否设置了 isEnterprise: true,但部署并非企业版?
  5. 检查主体名称——在 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。

评论

支持 Markdown