ld-permissions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Permissions & Authorization Guide

权限与授权指南

This skill helps you work with Lightdash's CASL-based permissions system, including scopes, custom roles, and authorization enforcement.
本指南可帮助你使用Lightdash基于CASL的权限系统,包括作用域(scopes)、自定义角色和授权规则实施。

What do you need help with?

你需要哪方面的帮助?

  1. Add a new scope/permission - Step-by-step guide to add a new permission
  2. Debug a permission issue - Troubleshoot why a user can't access something
  3. Understand the permission flow - Learn how permissions work end-to-end
  4. Work with custom roles - Create or modify custom roles with specific scopes
  1. 添加新作用域/权限 - 添加新权限的分步指南
  2. 调试权限问题 - 排查用户无法访问资源的原因
  3. 理解权限流转逻辑 - 了解权限的端到端工作机制
  4. 使用自定义角色 - 创建或修改带有特定作用域的自定义角色

Quick Reference

快速参考

Key Files

关键文件

PurposeLocation
Scope definitions
packages/common/src/authorization/scopes.ts
CASL types
packages/common/src/authorization/types.ts
Ability builder (system role vs custom role path)
packages/common/src/authorization/index.ts
System role abilities (project level)
packages/common/src/authorization/projectMemberAbility.ts
System role abilities (org level)
packages/common/src/authorization/organizationMemberAbility.ts
Service account abilities (enterprise, CI/CD)
packages/common/src/authorization/serviceAccountAbility.ts
Role-to-scope mapping
packages/common/src/authorization/roleToScopeMapping.ts
Scope-to-CASL conversion
packages/common/src/authorization/scopeAbilityBuilder.ts
用途文件路径
作用域定义
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

Common Patterns

常见模式

Backend permission check (services take
account: RegisteredAccount
and build the ability via
this.createAuditedAbility(account)
— raw
user.ability
is legacy, see
docs/account-patterns.md
):
typescript
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');
}
后端权限校验(服务接收
account: RegisteredAccount
并通过
this.createAuditedAbility(account)
构建权限能力——原生
user.ability
为旧版实现,请查看
docs/account-patterns.md
):
typescript
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 Subject Scoping: Resource, Not Actor

CASL主体作用域:针对资源,而非操作者

CASL actor is passed before the check:
typescript
getUserAbilityBuilder({
    user: lightdashUser, // actor
    projectProfiles,
    permissionsConfig,
});

const ability = this.createAuditedAbility(accountOrUser); // actor
subject(...)
must describe only the target resource:
typescript
ability.can(
    'manage',
    subject('X', {
        organizationUuid: target.organizationUuid,
        projectUuid: target.projectUuid,
    }),
);
Never fill
subject(...)
from actor fields like
user.organizationUuid
. Org-level grants may only check
organizationUuid
, so actor-sourced subject fields can become cross-org access on multi-org instances. Single-org dev hides it.
Frontend permission check:
typescript
const { user } = useApp();

if (user.data?.ability.can('manage', 'Dashboard')) {
    return <EditButton />;
}
or wrap in a CASL component:
tsx
import { Can } from '../../providers/Ability';

<Can I="manage" a="Dashboard">
    <EditButton />
</Can>
CASL操作者需在校验前传入:
typescript
getUserAbilityBuilder({
    user: lightdashUser, // 操作者
    projectProfiles,
    permissionsConfig,
});

const ability = this.createAuditedAbility(accountOrUser); // 操作者
subject(...)
必须仅描述目标资源:
typescript
ability.can(
    'manage',
    subject('X', {
        organizationUuid: target.organizationUuid,
        projectUuid: target.projectUuid,
    }),
);
切勿从操作者字段(如
user.organizationUuid
)填充
subject(...)
。组织级权限可能仅校验
organizationUuid
,因此来源于操作者的主体字段会在多组织实例中导致跨组织访问问题。单组织开发环境会隐藏此问题。
前端权限校验:
typescript
const { user } = useApp();

if (user.data?.ability.can('manage', 'Dashboard')) {
    return <EditButton />;
}
或用CASL组件包裹:
tsx
import { Can } from '../../providers/Ability';

<Can I="manage" a="Dashboard">
    <EditButton />
</Can>

Full Documentation

完整文档

For comprehensive documentation, read:
.context/PERMISSIONS.md
This includes:
  • Architecture diagram showing the complete permission flow
  • All scope groups and modifiers (@self, @public, @space, etc.)
  • Database schema for custom roles
  • Step-by-step guide to add new scopes
  • Troubleshooting guide
如需全面文档,请阅读:
.context/PERMISSIONS.md
其中包括:
  • 展示完整权限流转的架构图
  • 所有作用域组及修饰符(@self、@public、@space等)
  • 自定义角色的数据库 schema
  • 添加新作用域的分步指南
  • 故障排查指南

Adding a New Scope (Quick Guide)

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

You must update ALL the relevant ability layers:
  1. Add subject (if new) to
    CaslSubjectNames
    in
    packages/common/src/authorization/types.ts
  2. Define scope in
    packages/common/src/authorization/scopes.ts
    :
typescript
{
    name: 'manage:NewFeature',
    description: 'Description for custom role UI',
    isEnterprise: false,
    group: ScopeGroup.PROJECT_MANAGEMENT,
    getConditions: (context) => [addUuidCondition(context)],
}
  1. Update project-level abilities in
    packages/common/src/authorization/projectMemberAbility.ts
    — add to the appropriate system role function (e.g.,
    developer
    ,
    admin
    )
  2. Update org-level abilities in
    packages/common/src/authorization/organizationMemberAbility.ts
    if needed — note: org-level abilities are additive and cannot be restricted by project-level custom roles
  3. Add to system role in
    BASE_ROLE_SCOPES
    in
    packages/common/src/authorization/roleToScopeMapping.ts
    (must stay in sync with
    projectMemberAbility.ts
    — the parity test
    roleToScopeParity.test.ts
    enforces this)
  4. Update service accounts in
    packages/common/src/authorization/serviceAccountAbility.ts
    — add to
    ORG_ADMIN
    (or other service account scopes) if service accounts need this permission. Forgetting this breaks CI/CD pipelines.
  5. Enforce in service via
    this.createAuditedAbility(account)
    +
    ability.cannot()
    — never raw
    user.ability
    (legacy pattern, see
    docs/account-patterns.md
    )
  6. Add frontend check with
    useApp()
    user.data?.ability.can()
你必须更新所有相关的权限能力层:
  1. 添加主体类型(若为新类型)到
    packages/common/src/authorization/types.ts
    中的
    CaslSubjectNames
  2. 定义作用域
    packages/common/src/authorization/scopes.ts
    中:
typescript
{
    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()

Changing the Scope Vocabulary (Migrating Custom Roles)

修改作用域术语(自定义角色迁移)

Custom roles persist scope names as strings in the
scoped_roles
table (
role_uuid
,
scope_name
,
granted_by
). They are decoupled from system roles and do not auto-update when the scope vocabulary changes. Any rename / split / merge / removal must include a Knex migration that reconciles existing rows, otherwise self-hosted instances silently lose or retain permissions.
Before merging a scope change, evaluate the impact and write a migration:
ChangeImpact on
scoped_roles
Required migration
Rename a scope (e.g.
manage:Foo
manage:Bar
)
Old rows reference a name that no longer exists in
scopes.ts
.
parseScopes
drops them as invalid, silently revoking access.
UPDATE scoped_roles SET scope_name = 'new' WHERE scope_name = 'old'
Split one scope into two (e.g.
manage:CustomSql
manage:CustomSql
+
manage:CustomFields
)
Roles with the original scope lose access to whichever capability moved to the new scope.Backfill the new scope for every role that has the original (
INSERT ... SELECT ... ON CONFLICT DO NOTHING
). See
20260417111420_grant_custom_fields_to_custom_sql_roles.ts
.
Merge two scopes into oneRoles with only one of the merged scopes may gain or lose capability.Insert the merged scope where either source exists; then delete the old rows.
Remove a scopeRows reference a non-existent scope name.
parseScopes
silently drops them;
UserModel
logs "Custom role(s) for user ... reference scopes not in the runtime vocabulary" warnings on every ability build.
Delete the orphaned rows. See
20260519142606_remove_legacy_dashboard_export_scopes.ts
.
Tighten conditions on an existing scopeNo row change needed, but the behavioral change is invisible to operators.None on the table; note in PR description.
Add a brand-new scopeNo existing rows are affected. Only system roles in
roleToScopeMapping.ts
need updating.
None for custom roles.
Migration conventions (see
packages/backend/src/database/CLAUDE.md
for general safe-migration rules):
  • Wrap the body in
    try/catch
    and log a recoverable manual-fix command on failure. These backfills are best-effort cleanup — failing them should never block subsequent migrations.
  • Use
    ON CONFLICT DO NOTHING
    for inserts since
    (role_uuid, scope_name)
    is the natural unique key.
  • Preserve
    granted_by
    from the source row when copying a scope, so audit history points back at the original grantor rather than
    NULL
    .
  • Provide a sensible
    down()
    — usually deleting the rows the
    up()
    inserted. If the change is irreversible (legacy cleanup), document why
    down()
    is a no-op.
Checklist when changing the scope vocabulary:
  1. Determine which change type applies (rename / split / merge / remove / add / tighten).
  2. If a migration is required, create it with
    pnpm -F backend create-migration <name>
    and follow the patterns above.
  3. Update
    roleToScopeMapping.ts
    so system roles reflect the new vocabulary, and run the parity test.
  4. Call this out in the PR description so reviewers can verify the data migration matches the code change.
自定义角色的作用域名称以字符串形式存储在
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
在每次构建权限能力时都会记录"用户...的自定义角色引用了运行时术语中不存在的作用域"警告。
删除孤立的数据行。示例请见
20260519142606_remove_legacy_dashboard_export_scopes.ts
收紧现有作用域的条件无需修改数据行,但行为变更对运维人员不可见。无需修改表;在PR描述中注明。
添加全新作用域现有数据行不受影响。仅需更新
roleToScopeMapping.ts
中的系统角色。
无需针对自定义角色进行迁移。
迁移约定(查看
packages/backend/src/database/CLAUDE.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描述中注明此变更,以便评审人员验证数据迁移与代码变更是否匹配。

Debugging Permission Issues

调试权限问题

When a user gets "ForbiddenError":
  1. Check scope exists - Is the scope defined in
    scopes.ts
    ?
  2. Check role assignment - Does the user's role include this scope?
  3. Check conditions - Do the CASL conditions match the resource?
  4. Check enterprise flag - Is
    isEnterprise: true
    but deployment isn't enterprise?
  5. Check subject name - Case-sensitive match in
    CaslSubjectNames
    ?
Use grep to find where the permission is checked:
bash
grep -r "ability.cannot.*'manage'.*'YourSubject'" packages/backend/src/services/
Please describe what you're trying to accomplish, or ask me to explain any aspect of the permissions system.
当用户遇到"ForbiddenError"时:
  1. 检查作用域是否存在 - 该作用域是否在
    scopes.ts
    中定义?
  2. 检查角色分配 - 用户的角色是否包含此作用域?
  3. 检查条件匹配 - CASL条件是否与资源匹配?
  4. 检查企业版标记 - 是否设置了
    isEnterprise: true
    但部署的并非企业版?
  5. 检查主体名称 - 是否与
    CaslSubjectNames
    中的名称大小写匹配?
使用grep查找权限校验的位置:
bash
grep -r "ability.cannot.*'manage'.*'YourSubject'" packages/backend/src/services/
请描述你想要实现的目标,或让我解释权限系统的任何方面。