ld-permissions について
Lightdash の 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を入力して読み込まれたか確認します。
詳細
権限と認可のガイド
このスキルでは、スコープ、カスタムロール、認可の強制を含む、Lightdash の CASL ベースの権限システムについて説明します。
どのようなサポートが必要ですか?
- 新しいスコープ/権限を追加する - 新しい権限を追加するための手順ガイド
- 権限の問題をデバッグする - ユーザーが何かにアクセスできない理由を調査する
- 権限のフローを理解する - 権限がエンドツーエンドでどのように機能するかを学ぶ
- カスタムロールを扱う - 特定のスコープを持つカスタムロールを作成または変更する
クイックリファレンス
主なファイル
| 用途 | 場所 |
|---|---|
| スコープ定義 | packages/common/src/authorization/scopes.ts |
| CASL 型 | packages/common/src/authorization/types.ts |
| Ability ビルダー(システムロールとカスタムロールの処理経路) | packages/common/src/authorization/index.ts |
| システムロールの Ability(プロジェクトレベル) | packages/common/src/authorization/projectMemberAbility.ts |
| システムロールの Ability(組織レベル) | packages/common/src/authorization/organizationMemberAbility.ts |
| サービスアカウントの Ability(エンタープライズ、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) を使って Ability を構築します。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 の Subject スコープ設定:対象はリソースであり、実行者ではない
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 のみをチェックする場合があり、そのためアクター由来の subject フィールドによって、複数の組織があるインスタンスで組織をまたいだアクセスが可能になることがあります。単一組織の開発環境では、この問題が表面化しません。
フロントエンドの権限チェック:
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 など)
- カスタムロールのデータベーススキーマ
- 新しいスコープを追加する手順ガイド
- トラブルシューティングガイド
新しいスコープを追加する(クイックガイド)
関連する Ability レイヤーをすべて更新する必要があります。
-
新しい場合は Subject を追加する:
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)],
}
-
プロジェクトレベルの Ability を更新する:
packages/common/src/authorization/projectMemberAbility.tsの適切なシステムロール関数(例:developer、admin)に追加します。 -
必要に応じて組織レベルの Ability を更新する:
packages/common/src/authorization/organizationMemberAbility.tsを更新します。組織レベルの Ability は加算的であり、プロジェクトレベルのカスタムロールでは制限できないことに注意してください。 -
システムロールに追加する:
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' |
1つのスコープを2つに分割する(例:manage:CustomSql → manage:CustomSql + manage:CustomFields) | 元のスコープを持つロールは、新しいスコープに移された機能へのアクセスを失います。 | 元のスコープを持つすべてのロールに新しいスコープを追加します(INSERT ... SELECT ... ON CONFLICT DO NOTHING)。20260417111420_grant_custom_fields_to_custom_sql_roles.tsを参照してください。 |
| 2つのスコープを1つに統合する | 統合対象のスコープのうち一方しか持たないロールは、機能を得る場合も失う場合もあります。 | どちらかの元スコープが存在する場合に統合後のスコープを挿入し、その後、古い行を削除します。 |
| スコープを削除する | 行は存在しないスコープ名を参照します。parseScopesはそれらを気付かれないまま除外します。また、権限の構築時に毎回、UserModelが「ユーザー ... のカスタムロールが、ランタイムの語彙にないスコープを参照しています」という警告をログに記録します。 | 孤立した行を削除します。20260519142606_remove_legacy_dashboard_export_scopes.tsを参照してください。 |
| 既存スコープの条件を厳しくする | 行の変更は不要ですが、動作の変更は運用担当者には見えません。 | テーブルへの変更は不要です。PRの説明に記載してください。 |
| まったく新しいスコープを追加する | 既存の行には影響しません。更新が必要なのはroleToScopeMapping.tsのシステムロールのみです。 | カスタムロール向けには不要です。 |
マイグレーションの規約(安全なマイグレーション全般のルールについてはpackages/backend/src/database/AGENTS.mdを参照してください):
- 本体を
try/catchで囲み、失敗時には手動で修正できるコマンドをログに記録してください。これらのバックフィルはベストエフォートのクリーンアップです。失敗しても後続のマイグレーションを決して妨げないようにしてください。 (role_uuid, scope_name)は自然な一意キーなので、挿入にはON CONFLICT DO NOTHINGを使用してください。- スコープをコピーする際は、元の行の
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を呼び出します。 - 境界を越える操作(スペースへの移動、昇格、content-as-code、ピン留め、別のダッシュボードへのコピー)→
uuid: nullを渡すか、getSpaceAccessContextを使用します。付与が決して権限として扱われないようにしてください。expectNoGrantRowsテストのトリップワイヤーがこうした操作を保護しており、境界を越える操作で付与を経由するとテストが失敗します。 - 付与元のダッシュボードが属していない
spaceUuidに対して、付与を解決してはなりません。このヘルパーはこの組み合わせをアサートします。
書き込みと境界を越える操作の区分は SavedChartService にあります(付与に対応:update、createVersion、create、delete、softDelete、duplicate。スペースのみ:moveToSpace、updateMultiple の移動部分、ピン留め)。packages/backend/src/services/SpaceService/AGENTS.md も参照してください。