LD

ld-permissions

Troubleshoot and configure Lightdash permissions and CASL-based authorization flows.

Install

mkdir -p .claude/skills/ld-permissions && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/6561" && unzip -o skill.zip -d .claude/skills/ld-permissions && rm skill.zip

Installs to .claude/skills/ld-permissions

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Guide for Lightdash's CASL-based authorization system. Use when working with scopes, custom roles, abilities, permissions, ForbiddenError, authorization, or access control. Helps with adding new scopes, debugging permission issues, understanding the permission flow, and creating custom roles.
293 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Advanced

Key capabilities

  • Add new permission scopes
  • Debug user access issues
  • Configure custom roles
  • Enforce CASL abilities in backend services

How it works

The skill guides updates to ability layers, scope definitions, and role mappings, ensuring parity between system roles and custom roles.

Inputs & outputs

You give it
Permission requirement or ForbiddenError
You get back
Updated CASL ability configuration

When to use ld-permissions

  • Debug user access issues
  • Define new permission scopes
  • Create custom roles
  • Configure service account abilities

About this skill

Permissions & Authorization Guide

This skill helps you work with Lightdash's CASL-based permissions system, including scopes, custom roles, and authorization enforcement.

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

Quick Reference

Key Files

PurposeLocation
Scope definitionspackages/common/src/authorization/scopes.ts
CASL typespackages/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 mappingpackages/common/src/authorization/roleToScopeMapping.ts
Scope-to-CASL conversionpackages/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):

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 actor is passed before the check:

getUserAbilityBuilder({
    user: lightdashUser, // actor
    projectProfiles,
    permissionsConfig,
});

const ability = this.createAuditedAbility(accountOrUser); // actor

subject(...) must describe only the target resource:

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:

const { user } = useApp();

if (user.data?.ability.can('manage', 'Dashboard')) {
    return <EditButton />;
}

or wrap in a CASL component:

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

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:

{
    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()

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_rolesRequired migration
Rename a scope (e.g. manage:Foomanage: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:CustomSqlmanage: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.

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:

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.

When not to use it

  • General database schema changes
  • Frontend UI development unrelated to permissions

Prerequisites

Access to Lightdash codebase

Limitations

  • Custom roles do not auto-update when scope vocabulary changes
  • Org-level abilities cannot be restricted by project-level roles

How it compares

It provides a systematic approach to updating the CASL-based authorization system rather than manual policy editing.

Compared to similar skills

ld-permissions side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
ld-permissions (this skill)328dReviewAdvanced
reverse-engineering-tools734moNo flagsAdvanced
game-hacking-techniques422moNo flagsAdvanced
solidity-security152moNo flagsIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

You might also like

reverse-engineering-tools

gmh5225

Guide for reverse engineering tools and techniques used in game security research. Use this skill when working with debuggers, disassemblers, memory analysis tools, binary analysis, or decompilers for game security research.

73204

game-hacking-techniques

gmh5225

Guide for game hacking techniques and cheat development. Use this skill when researching memory manipulation, code injection, ESP/aimbot development, overlay rendering, or game exploitation methodologies.

42128

solidity-security

wshobson

Master smart contract security best practices to prevent common vulnerabilities and implement secure Solidity patterns. Use when writing smart contracts, auditing existing contracts, or implementing security measures for blockchain applications.

15115

1password

openclaw

Set up and use 1Password CLI (op). Use when installing the CLI, enabling desktop app integration, signing in (single or multi-account), or reading/injecting/running secrets via op.

2799

senior-security

davila7

Comprehensive security engineering skill for application security, penetration testing, security architecture, and compliance auditing. Includes security assessment tools, threat modeling, crypto implementation, and security automation. Use when designing security architecture, conducting penetration tests, implementing cryptography, or performing security audits.

3191

ghidra

mitsuhiko

Reverse engineer binaries using Ghidra's headless analyzer. Decompile executables, extract functions, strings, symbols, and analyze call graphs without GUI.

16105

Search skills

Search the agent skills registry