or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

api

features

charts

charts.mdconditional-formatting.mdvisualizations.md
authorization.mdchangesets.mdcharts-as-code.mdcompiler.mddashboards.mddbt.mdee-features.mdformatting.mdparameters.mdpivot.mdprojects-spaces.mdsql-runner.mdtemplating.mdwarehouse.md
index.md
tile.json

authorization.mddocs/api/features/

Authorization and Permissions

CASL-based authorization system with role-based access control, scopes, and abilities for users, service accounts, and embedded contexts.

Overview

Lightdash uses CASL (an isomorphic authorization library) to manage permissions across the platform. The authorization system supports:

  • Role-based access control (organization, project, and space roles)
  • Granular permission scopes for features
  • User abilities with project-specific profiles
  • Service account permissions
  • Embedded dashboard/chart access control
  • JWT token-based authentication for embedded content

Imports

import {
  // User ability functions
  defineUserAbility,
  getUserAbilityBuilder,

  // Account helper functions
  buildAccountHelpers,
  parseAccount,

  // JWT authentication
  JWT_HEADER_NAME,
  applyEmbeddedAbility,

  // Service account abilities
  applyServiceAccountAbilities,
  ServiceAccountScope,

  // Scope management
  getScopes,
  getAllScopeMap,
  parseScope,
  normalizeScopeName,
  parseScopes,
  buildAbilityFromScopes,

  // Role to scope mapping
  getAllScopesForRole,
  getNonEnterpriseScopesForRole,
  getSystemRoles,
  isSystemRole,
  PROJECT_ROLE_TO_SCOPES_MAP,

  // Types
  type MemberAbility,
  type AbilityAction,
  type CaslSubjectNames,
  type Subject,
  type PossibleAbilities,
  type Scope,
  type ScopeName,
  type ScopeContext,
  type ScopeGroup,
  type ScopeModifer,
  type Account,
  type SessionAccount,
  type AnonymousAccount,
  type ApiKeyAccount,
  type ServiceAcctAccount,
  type OauthAccount,
  type AccountHelpers,
  type AccountWithoutHelpers,
  type Authentication,
  type SessionAuth,
  type JwtAuth,
  type ServiceAccountAuth,
  type PersonalAccessTokenAuth,
  type OauthAuth,
  type AuthType,
  AuthTokenPrefix,         // enum
  type EmbedContent,
  type EmbedAccess,
  type UserAccessControls,
  type OssEmbed,
  OrganizationMemberRole,  // enum
  ProjectMemberRole,       // enum
  type LightdashUser,
  type ProjectMemberProfile,
  type Organization,
  type Role,
  type RoleWithScopes,
  type CreateEmbedJwt,
} from '@lightdash/common';

Note: OrganizationMemberRole, ProjectMemberRole, and AuthTokenPrefix are enums, accessed as OrganizationMemberRole.ADMIN, ProjectMemberRole.EDITOR, AuthTokenPrefix.SERVICE_ACCOUNT, etc.

Core Concepts

Actions and Subjects

type AbilityAction =
  | 'create'
  | 'delete'
  | 'export'
  | 'manage'
  | 'promote'
  | 'update'
  | 'view';

type CaslSubjectNames =
  | 'AiAgent'
  | 'AiAgentThread'
  | 'Analytics'
  | 'ChangeCsvResults'
  | 'CompileProject'
  | 'ContentAsCode'
  | 'CustomSql'
  | 'Dashboard'
  | 'DashboardComments'
  | 'DashboardCsv'
  | 'DashboardImage'
  | 'DashboardPdf'
  | 'Explore'
  | 'ExportCsv'
  | 'GoogleSheets'
  | 'Group'
  | 'InviteLink'
  | 'Job'
  | 'JobStatus'
  | 'MetricsTree'
  | 'Organization'
  | 'OrganizationMemberProfile'
  | 'OrganizationWarehouseCredentials'
  | 'PersonalAccessToken'
  | 'PinnedItems'
  | 'Project'
  | 'SavedChart'
  | 'ScheduledDeliveries'
  | 'SemanticViewer'
  | 'Space'
  | 'SpotlightTableConfig'
  | 'SqlRunner'
  | 'Tags'
  | 'UnderlyingData'
  | 'Validation'
  | 'VirtualView';
  // 37 total subject types

type Subject =
  | CaslSubjectNames
  | Project
  | Organization
  | OrganizationMemberProfile
  | 'all';

type PossibleAbilities = [AbilityAction, Subject | ForcedSubject<Exclude<Subject, 'all'>>];

type MemberAbility = Ability<PossibleAbilities>;

User Ability Building

defineUserAbility

Creates a complete user ability for authorization checks.

function defineUserAbility(
  user: Pick<LightdashUser, 'role' | 'organizationUuid' | 'userUuid' | 'roleUuid'>,
  projectProfiles: Pick<ProjectMemberProfile, 'projectUuid' | 'role' | 'userUuid' | 'roleUuid'>[],
  customRoleScopes?: Record<Role['roleUuid'], RoleWithScopes['scopes']>
): MemberAbility;

Parameters:

  • user: Session user with organization role
    • roleUuid is optional and only used for custom roles
  • projectProfiles: Array of project member profiles (can be empty array)
    • roleUuid in each profile is optional and only used for custom roles
  • customRoleScopes: Optional custom role scope mappings

Returns a CASL Ability instance that can be used to check permissions.

Example:

import { defineUserAbility } from '@lightdash/common';

const ability = defineUserAbility(
  sessionUser,
  [
    {
      projectUuid: 'project-1',
      role: ProjectMemberRole.EDITOR,
      userUuid: sessionUser.userUuid,
    },
  ]
);

// Check permissions
if (ability.can('view', 'Dashboard')) {
  // User can view dashboards
}

if (ability.can('update', { subject: 'SavedChart', access: [] })) {
  // User can update saved charts
}

getUserAbilityBuilder

Gets the ability builder for constructing custom abilities.

/**
 * Get an ability builder for defining user permissions. The ability builder allows granular
 * permission configuration before finalizing with ability.build().
 *
 * @param args - User ability configuration with the following structure:
 *   - user: Pick<LightdashUser, 'role' | 'organizationUuid' | 'userUuid' | 'roleUuid'>
 *   - projectProfiles: Pick<ProjectMemberProfile, 'projectUuid' | 'role' | 'userUuid' | 'roleUuid'>[]
 *   - permissionsConfig: { pat: { enabled: boolean; allowedOrgRoles: OrganizationMemberRole[] } }
 *   - customRoleScopes?: Record<Role['roleUuid'], RoleWithScopes['scopes']>
 *   - customRolesEnabled?: boolean
 *   - isEnterprise?: boolean
 *
 * @returns AbilityBuilder instance for constructing user permissions
 */
function getUserAbilityBuilder(args: {
  user: Pick<LightdashUser, 'role' | 'organizationUuid' | 'userUuid' | 'roleUuid'>;
  projectProfiles: Pick<ProjectMemberProfile, 'projectUuid' | 'role' | 'userUuid' | 'roleUuid'>[];
  permissionsConfig: {
    pat: {
      enabled: boolean;
      allowedOrgRoles: OrganizationMemberRole[];
    };
  };
  customRoleScopes?: Record<Role['roleUuid'], RoleWithScopes['scopes']>;
  customRolesEnabled?: boolean;
  isEnterprise?: boolean;
}): AbilityBuilder<MemberAbility>;

Note: The return type AbilityBuilder<MemberAbility> uses AbilityBuilder from the external @casl/ability package (CASL is a peer dependency). The MemberAbility type IS exported from @lightdash/common.

Example:

import { getUserAbilityBuilder } from '@lightdash/common';

const builder = getUserAbilityBuilder({
  user: {
    role: OrganizationMemberRole.ADMIN,
    organizationUuid: 'org-uuid',
    userUuid: 'user-uuid',
    roleUuid: undefined,
  },
  projectProfiles: userProjectProfiles,
  permissionsConfig: {
    pat: {
      enabled: true,
      allowedOrgRoles: [OrganizationMemberRole.ADMIN],
    },
  },
  customRoleScopes: customRoles,
  customRolesEnabled: true,
  isEnterprise: true,
});

// builder.can(), builder.cannot() methods available
const ability = builder.build();

Scopes and Roles

Role and Scope Utilities

Functions for working with project member roles and their associated scopes. These utilities help map roles to permissions and determine role types.

getAllScopesForRole

Gets all scopes (including inherited) for a project member role.

function getAllScopesForRole(role: ProjectMemberRole): string[];

Parameters:

  • role: Project member role (VIEWER, INTERACTIVE_VIEWER, EDITOR, DEVELOPER, or ADMIN)

Returns: Array of all scope names for the role, including inherited scopes from lower roles in the hierarchy.

Role Hierarchy: Roles inherit scopes from lower-level roles:

  1. VIEWER (base role)
  2. INTERACTIVE_VIEWER (inherits VIEWER scopes)
  3. EDITOR (inherits INTERACTIVE_VIEWER + VIEWER scopes)
  4. DEVELOPER (inherits EDITOR + INTERACTIVE_VIEWER + VIEWER scopes)
  5. ADMIN (inherits all lower role scopes)

Example:

import { getAllScopesForRole, ProjectMemberRole } from '@lightdash/common';

// Get all scopes for editor role
const editorScopes = getAllScopesForRole(ProjectMemberRole.EDITOR);
console.log('Editor has', editorScopes.length, 'scopes');
// Output: Editor has 23 scopes (includes viewer + interactive viewer + editor scopes)

// Check if a specific scope is included
if (editorScopes.includes('view:Dashboard')) {
  console.log('Editor can view dashboards');
}

// Compare scope counts across roles
const viewerScopes = getAllScopesForRole(ProjectMemberRole.VIEWER);
const adminScopes = getAllScopesForRole(ProjectMemberRole.ADMIN);
console.log(`Viewer: ${viewerScopes.length}, Editor: ${editorScopes.length}, Admin: ${adminScopes.length}`);
// Output shows progressive scope expansion

getNonEnterpriseScopesForRole

Gets only non-enterprise scopes for a role, filtering out enterprise-only features.

function getNonEnterpriseScopesForRole(role: ProjectMemberRole): string[];

Parameters:

  • role: Project member role to get non-enterprise scopes for

Returns: Array of scope names excluding enterprise features. This includes all inherited scopes from lower roles, but filters out enterprise-only capabilities.

Filtered Enterprise Scopes:

  • view:MetricsTree
  • manage:MetricsTree
  • view:SpotlightTableConfig
  • manage:SpotlightTableConfig
  • view:AiAgent
  • view:AiAgentThread
  • create:AiAgentThread
  • manage:AiAgent
  • manage:AiAgentThread
  • manage:ContentAsCode
  • manage:PersonalAccessToken

Example:

import {
  getAllScopesForRole,
  getNonEnterpriseScopesForRole,
  ProjectMemberRole
} from '@lightdash/common';

const role = ProjectMemberRole.DEVELOPER;

// Get all scopes (including enterprise)
const allScopes = getAllScopesForRole(role);

// Get only non-enterprise scopes
const nonEnterpriseScopes = getNonEnterpriseScopesForRole(role);

console.log(`Total scopes: ${allScopes.length}`);
console.log(`Non-enterprise scopes: ${nonEnterpriseScopes.length}`);
console.log(`Enterprise scopes: ${allScopes.length - nonEnterpriseScopes.length}`);

// Use for non-enterprise installations
const scopesForCommunityEdition = getNonEnterpriseScopesForRole(
  ProjectMemberRole.EDITOR
);

getSystemRoles

Gets all system roles with their complete metadata and associated scopes.

function getSystemRoles(): RoleWithScopes[];

Returns: Array of all system roles (Viewer, Interactive Viewer, Editor, Developer, Admin) with their metadata and scopes. Each role object includes:

interface RoleWithScopes {
  roleUuid: ProjectMemberRole;  // Role identifier (e.g., 'viewer', 'editor')
  name: string;                  // Display name (e.g., 'Viewer', 'Editor')
  description: string;           // Role description (same as name for system roles)
  ownerType: 'system';          // Always 'system' for built-in roles
  scopes: string[];             // Array of all scopes for this role
  organizationUuid: null;       // System roles aren't org-specific
  createdAt: null;              // System roles don't have creation dates
  updatedAt: null;              // System roles don't have update dates
  createdBy: null;              // System roles aren't created by users
}

Important: The returned objects have a roleUuid property (not role), which contains the ProjectMemberRole value.

Example:

import { getSystemRoles, isSystemRole } from '@lightdash/common';

// Get all system roles
const systemRoles = getSystemRoles();

// Display role information
systemRoles.forEach((role) => {
  console.log(`${role.name} (${role.roleUuid}): ${role.scopes.length} scopes`);
});
// Output:
// Viewer (viewer): 11 scopes
// Interactive Viewer (interactive_viewer): 21 scopes
// Editor (editor): 28 scopes
// Developer (developer): 37 scopes
// Admin (admin): 44 scopes

// Find a specific role
const editorRole = systemRoles.find(r => r.roleUuid === 'editor');
if (editorRole) {
  console.log('Editor scopes:', editorRole.scopes);
}

// Create a role picker UI
const roleOptions = systemRoles.map(role => ({
  value: role.roleUuid,
  label: role.name,
  scopeCount: role.scopes.length,
}));

isSystemRole

Type guard that checks if a role UUID is a built-in system role.

function isSystemRole(roleUuid: string): roleUuid is ProjectMemberRole;

Parameters:

  • roleUuid: Role identifier string to check

Returns: true if the roleUuid is a system role (viewer, interactive_viewer, editor, developer, or admin), false otherwise. This function uses a TypeScript type predicate to narrow the type to ProjectMemberRole when it returns true.

Note: This check is case-sensitive. System role identifiers are lowercase (e.g., 'viewer', not 'VIEWER').

Example:

import { isSystemRole, getAllScopesForRole, ProjectMemberRole } from '@lightdash/common';

// Check if a role is a system role
const roleId = 'editor';
if (isSystemRole(roleId)) {
  // TypeScript now knows roleId is ProjectMemberRole
  const scopes = getAllScopesForRole(roleId);
  console.log('System role with', scopes.length, 'scopes');
} else {
  console.log('Custom role - fetch scopes from database');
}

// Validate role assignments
function assignRole(userId: string, roleUuid: string) {
  if (isSystemRole(roleUuid)) {
    // Handle system role assignment
    console.log('Assigning system role:', roleUuid);
  } else {
    // Handle custom role assignment
    console.log('Assigning custom role:', roleUuid);
  }
}

// Filter system roles from custom roles
const roleUuids = ['viewer', 'custom-uuid-123', 'editor', 'custom-uuid-456'];
const systemRoleIds = roleUuids.filter(isSystemRole);
const customRoleIds = roleUuids.filter(id => !isSystemRole(id));

console.log('System roles:', systemRoleIds); // ['viewer', 'editor']
console.log('Custom roles:', customRoleIds); // ['custom-uuid-123', 'custom-uuid-456']

// Case sensitivity check
console.log(isSystemRole('viewer'));   // true
console.log(isSystemRole('VIEWER'));   // false (case matters!)
console.log(isSystemRole('Editor'));   // false (case matters!)

PROJECT_ROLE_TO_SCOPES_MAP

Constant mapping of project member roles to their complete scope arrays.

const PROJECT_ROLE_TO_SCOPES_MAP: Record<ProjectMemberRole, string[]>;

This constant provides direct access to the role-to-scope mapping used internally by getAllScopesForRole(). It's a frozen object that maps each ProjectMemberRole enum value to its array of scopes (including inherited scopes).

Example:

import { PROJECT_ROLE_TO_SCOPES_MAP, ProjectMemberRole } from '@lightdash/common';

// Direct access to role scopes
const editorScopes = PROJECT_ROLE_TO_SCOPES_MAP[ProjectMemberRole.EDITOR];
const viewerScopes = PROJECT_ROLE_TO_SCOPES_MAP[ProjectMemberRole.VIEWER];

// Compare scopes between roles
const additionalEditorScopes = editorScopes.filter(
  scope => !viewerScopes.includes(scope)
);
console.log('Additional scopes for editor:', additionalEditorScopes);

// Check if a role has a specific scope
if (PROJECT_ROLE_TO_SCOPES_MAP[ProjectMemberRole.DEVELOPER].includes('manage:SqlRunner')) {
  console.log('Developers can use SQL runner');
}

// Get all unique scopes across all roles
const allScopes = new Set(
  Object.values(PROJECT_ROLE_TO_SCOPES_MAP).flat()
);
console.log('Total unique scopes:', allScopes.size);

Scopes System

Scopes

Scopes represent granular permissions for specific features.

interface Scope {
  name: ScopeName;
  description: string;
  group: ScopeGroup;
  isEnterprise: boolean;
  getConditions: (context: ScopeContext) => Record<string, unknown>[];
}

type BaseScopeContext = {
  userUuid: string;
  scopes: Set<ScopeName>;
  isEnterprise: boolean;
  organizationRole?: string;
  permissionsConfig?: {
    pat: {
      enabled: boolean;
      allowedOrgRoles: string[];
    };
  };
};

type OrganizationScopeContext = BaseScopeContext & {
  organizationUuid: string;
  projectUuid?: never;
};

type ProjectScopeContext = BaseScopeContext & {
  projectUuid: string;
  organizationUuid?: never;
};

type ScopeContext = OrganizationScopeContext | ProjectScopeContext;

type ScopeModifer = 'self' | 'public' | 'assigned' | 'space';

enum ScopeGroup {
  CONTENT = 'content',
  PROJECT_MANAGEMENT = 'project_management',
  ORGANIZATION_MANAGEMENT = 'organization_management',
  DATA = 'data',
  SHARING = 'sharing',
  AI = 'ai',
  SPOTLIGHT = 'spotlight',
}

type ScopeName =
  | 'view:Dashboard'
  | 'manage:Dashboard'
  | 'manage:Dashboard@space'
  | 'view:SavedChart'
  | 'manage:SavedChart'
  | 'manage:SavedChart@space'
  | 'view:Space'
  | 'create:Space'
  | 'manage:Space'
  | 'manage:Space@public'
  | 'manage:Space@assigned'
  | 'view:DashboardComments'
  | 'create:DashboardComments'
  | 'manage:DashboardComments'
  | 'view:Tags'
  | 'manage:Tags'
  | 'view:PinnedItems'
  | 'manage:PinnedItems'
  | 'promote:SavedChart'
  | 'promote:SavedChart@space'
  | 'promote:Dashboard'
  | 'promote:Dashboard@space'
  | 'view:Project'
  | 'create:Project'
  | 'update:Project'
  | 'delete:Project'
  | 'delete:Project@self'
  | 'manage:Project'
  | 'manage:CompileProject'
  | 'manage:Validation'
  | 'create:ScheduledDeliveries'
  | 'manage:ScheduledDeliveries'
  | 'manage:GoogleSheets'
  | 'view:Analytics'
  | 'create:Job'
  | 'view:Job'
  | 'view:Job@self'
  | 'manage:Job'
  | 'view:JobStatus'
  | 'view:JobStatus@self'
  | 'view:Organization'
  | 'manage:Organization'
  | 'view:OrganizationMemberProfile'
  | 'manage:OrganizationMemberProfile'
  | 'manage:InviteLink'
  | 'manage:Group'
  | 'manage:OrganizationWarehouseCredentials'
  | 'manage:ContentAsCode'
  | 'manage:PersonalAccessToken'
  | 'view:UnderlyingData'
  | 'view:SemanticViewer'
  | 'manage:SemanticViewer'
  | 'manage:SemanticViewer@space'
  | 'manage:Explore'
  | 'manage:SqlRunner'
  | 'manage:CustomSql'
  | 'create:VirtualView'
  | 'delete:VirtualView'
  | 'manage:VirtualView'
  | 'manage:ExportCsv'
  | 'manage:ChangeCsvResults'
  | 'export:DashboardCsv'
  | 'export:DashboardImage'
  | 'export:DashboardPdf'
  | 'view:AiAgent'
  | 'manage:AiAgent'
  | 'view:AiAgentThread'
  | 'view:AiAgentThread@self'
  | 'create:AiAgentThread'
  | 'manage:AiAgentThread'
  | 'manage:AiAgentThread@self'
  | 'manage:SpotlightTableConfig'
  | 'view:SpotlightTableConfig'
  | 'view:MetricsTree'
  | 'manage:MetricsTree';

function getScopes(options?: {
  isEnterprise?: boolean;
}): Scope[];

function getAllScopeMap(options?: {
  isEnterprise?: boolean;
}): Record<ScopeName, Scope>;

Example:

import { getScopes, getAllScopeMap } from '@lightdash/common';

// Get all scopes (including enterprise)
const allScopes = getScopes({ isEnterprise: true });

// Get scope map for lookups
const scopeMap = getAllScopeMap({ isEnterprise: false });
const viewDashboardScope = scopeMap['view:Dashboard'];

console.log(`${viewDashboardScope.name}: ${viewDashboardScope.description}`);

Scope Parsing and Validation

Functions for parsing, validating, and normalizing scope strings, and building abilities from scope arrays.

/**
 * Parse a scope string into its component parts (action, subject, modifier)
 * @param scope - Scope string in format "action:subject" or "action:subject@modifier"
 * @returns Tuple of [action, subject, modifier]
 */
function parseScope(
  scope: string
): [AbilityAction, CaslSubjectNames, ScopeModifer | undefined];

/**
 * Normalize a scope string to canonical format
 * @param scope - Scope string to normalize
 * @returns Normalized scope name
 */
function normalizeScopeName(scope: string): ScopeName;

/**
 * Parse and validate an array of scope strings
 * @param scopes - Array of scope strings to validate
 * @param isEnterprise - Whether to include enterprise scopes
 * @returns Set of validated scope names
 */
function parseScopes(options: {
  scopes: string[];
  isEnterprise: boolean;
}): Set<ScopeName>;

/**
 * Build CASL abilities from an array of scopes
 * @param context - Context with user, organization/project, and scopes
 * @param builder - CASL ability builder to add permissions to
 */
function buildAbilityFromScopes(
  context: {
    userUuid: string;
    scopes: string[];
    isEnterprise: boolean | undefined;
    organizationRole?: string;
    permissionsConfig?: {
      pat: {
        enabled: boolean;
        allowedOrgRoles: string[];
      };
    };
  } & (
    | { organizationUuid: string; projectUuid?: never }
    | { projectUuid: string; organizationUuid?: never }
  ),
  builder: AbilityBuilder<MemberAbility>
): void;

Example:

import {
  parseScope,
  normalizeScopeName,
  parseScopes,
  buildAbilityFromScopes,
  getUserAbilityBuilder,
} from '@lightdash/common';

// Parse individual scope string
const [action, subject, modifier] = parseScope('manage:Dashboard@space');
console.log(action); // 'manage'
console.log(subject); // 'Dashboard'
console.log(modifier); // 'space'

// Normalize scope name (converts variations to canonical format)
const normalized = normalizeScopeName('view:dashboard');
console.log(normalized); // 'view:Dashboard'

// Validate and parse array of scopes
const validScopes = parseScopes({
  scopes: ['view:Dashboard', 'manage:SavedChart', 'invalid:Scope'],
  isEnterprise: false,
});
console.log(validScopes); // Set { 'view:Dashboard', 'manage:SavedChart' }
// Note: 'invalid:Scope' is filtered out with console warning

// Build abilities from scopes
const builder = getUserAbilityBuilder();
buildAbilityFromScopes(
  {
    userUuid: 'user-123',
    organizationUuid: 'org-456',
    scopes: ['view:Dashboard', 'manage:SavedChart', 'view:Project'],
    isEnterprise: false,
    organizationRole: 'member',
  },
  builder
);
const ability = builder.build();

// Check permissions
console.log(ability.can('view', 'Dashboard')); // true
console.log(ability.can('manage', 'SavedChart')); // true
console.log(ability.can('delete', 'Project')); // false

Account Type System

Account Types

The authorization system supports multiple account types with different authentication methods.

type AuthType = 'session' | 'pat' | 'service-account' | 'jwt' | 'oauth';

enum AuthTokenPrefix {
  SCIM = 'scim_',
  SERVICE_ACCOUNT = 'ldsvc_',
  PERSONAL_ACCESS_TOKEN = 'ldpat_',
  OAUTH_APP = 'ldapp_',
  OAUTH_REFRESH = 'ldref_',
}

interface AccountHelpers {
  isAuthenticated: () => boolean;
  isRegisteredUser: () => boolean;
  isAnonymousUser: () => boolean;
  isSessionUser: () => boolean;
  isJwtUser: () => boolean;
  isServiceAccount: () => boolean;
  isPatUser: () => boolean;
  isOauthUser: () => boolean;
}

type AccountOrganization = Partial<
  Pick<Organization, 'organizationUuid' | 'name' | 'createdAt'>
>;

type SessionAuth = {
  type: 'session';
  source: string;
};

type PersonalAccessTokenAuth = {
  type: 'pat';
  source: string;
};

type ServiceAccountAuth = {
  type: 'service-account';
  source: string;
};

type JwtAuth = {
  type: 'jwt';
  data: CreateEmbedJwt;
  source: string;
};

type OauthAuth = {
  type: 'oauth';
  source: string;
  token: string;
  clientId: string;
  scopes: string[];
  expiresAt?: number;
  resource?: URL;
};

type Authentication =
  | SessionAuth
  | JwtAuth
  | ServiceAccountAuth
  | PersonalAccessTokenAuth
  | OauthAuth;

type BaseAccount = {
  organization: AccountOrganization;
  authentication: Authentication;
  user: AccountUser;
};

type SessionAccount = BaseAccount & AccountHelpers & {
  authentication: SessionAuth;
  user: LightdashSessionUser;
};

type AnonymousAccount = BaseAccount & AccountHelpers & {
  authentication: JwtAuth;
  user: ExternalUser;
  access: EmbedAccess;
  embed: OssEmbed;
};

type ApiKeyAccount = BaseAccount & AccountHelpers & {
  authentication: PersonalAccessTokenAuth;
  user: LightdashSessionUser;
};

type ServiceAcctAccount = BaseAccount & AccountHelpers & {
  authentication: ServiceAccountAuth;
  user: LightdashSessionUser;
};

type OauthAccount = BaseAccount & AccountHelpers & {
  authentication: OauthAuth;
  user: LightdashSessionUser;
};

type Account =
  | SessionAccount
  | AnonymousAccount
  | ApiKeyAccount
  | ServiceAcctAccount
  | OauthAccount;

type AccountWithoutHelpers<T extends Account> = Omit<T, keyof AccountHelpers>;

buildAccountHelpers

Creates helper methods for checking account type and authentication status.

/**
 * Build helper methods for an account object
 * @param account - Account without helper methods
 * @returns Object with helper methods for checking account type
 */
function buildAccountHelpers<T extends Account>(
  account: AccountWithoutHelpers<T>
): AccountHelpers;

Parameters:

  • account: Account object without helper methods

Returns an object with the following helper methods:

  • isAuthenticated(): Returns true if user is logged in
  • isRegisteredUser(): Returns true if account is for a known user in database
  • isAnonymousUser(): Returns true if account is for an anonymous external user
  • isSessionUser(): Returns true if account uses session authentication
  • isJwtUser(): Returns true if account uses JWT authentication
  • isServiceAccount(): Returns true if account is a service account
  • isPatUser(): Returns true if account uses personal access token
  • isOauthUser(): Returns true if account uses OAuth authentication

Example:

import { buildAccountHelpers } from '@lightdash/common';

const accountData = {
  organization: { organizationUuid: 'org-123' },
  authentication: { type: 'pat', source: 'token-abc' },
  user: sessionUser,
};

const helpers = buildAccountHelpers(accountData);

if (helpers.isPatUser()) {
  console.log('Authenticated with personal access token');
}

if (helpers.isAuthenticated()) {
  console.log('User is authenticated');
}

parseAccount

Reconstructs a full account object with abilities and helper methods from deserialized data.

/**
 * Reconstruct the full account with abilities and helper methods
 * @param deserializedAccount - Account data without helper methods or abilities
 * @returns Full account object with abilities and helpers
 */
function parseAccount(
  deserializedAccount: AccountWithoutHelpers<Account>
): Account;

Parameters:

  • deserializedAccount: Account data from storage/serialization without helper methods

Returns the appropriate account type (SessionAccount, ApiKeyAccount, AnonymousAccount, or ServiceAcctAccount) with:

  • Rebuilt CASL Ability from stored ability rules
  • Helper methods for checking account type
  • Full type information preserved

Example:

import { parseAccount } from '@lightdash/common';

// Deserialize account from session/database
const deserializedData = JSON.parse(sessionData);

// Reconstruct full account with abilities and helpers
const account = parseAccount(deserializedData);

// Use helper methods
if (account.isSessionUser()) {
  console.log('Session user:', account.user.email);
}

// Check permissions
if (account.user.ability.can('view', 'Dashboard')) {
  console.log('User can view dashboards');
}

Project Group Access Types

Types for managing group-level access control to projects.

/**
 * Project-level access granted to a group
 */
interface ProjectGroupAccess {
  /** UUID of the project */
  projectUuid: string;
  /** UUID of the group */
  groupUuid: string;
  /** Role assigned to the group (ProjectMemberRole enum or custom role UUID) */
  role: ProjectMemberRole | string;
}

/**
 * Data required to create project group access
 */
type CreateProjectGroupAccess = ProjectGroupAccess;

/**
 * Data required to update project group access
 */
type UpdateProjectGroupAccess = ProjectGroupAccess;

/**
 * Data required to delete project group access
 */
type DeleteProjectGroupAccess = Pick<
  ProjectGroupAccess,
  'projectUuid' | 'groupUuid'
>;

/**
 * API response for creating project group access
 */
interface ApiCreateProjectGroupAccess {
  status: 'ok';
  results: ProjectGroupAccess;
}

/**
 * API response for updating project group access
 */
interface ApiUpdateProjectGroupAccess {
  status: 'ok';
  results: ProjectGroupAccess;
}

JWT-Based Abilities

JWT_HEADER_NAME

HTTP header name for JWT tokens in embedded content requests.

const JWT_HEADER_NAME = 'lightdash-embed-token';

This constant defines the header name used to pass JWT tokens when accessing embedded dashboards and charts. The token is validated server-side to grant appropriate permissions.

Example:

import { JWT_HEADER_NAME } from '@lightdash/common';

// Client-side: Send request with JWT token
fetch('/api/v1/dashboards/uuid', {
  headers: {
    [JWT_HEADER_NAME]: embedToken,
  },
});

// Server-side: Extract token from request
const token = req.headers[JWT_HEADER_NAME];

applyEmbeddedAbility

Applies abilities for embedded dashboards and charts based on JWT permissions.

/**
 * Apply abilities for embedded content
 * @param embedUser - JWT user with embedded content permissions
 * @param content - The embedded content (dashboard or chart)
 * @param embed - Embed configuration
 * @param externalId - External customer ID for filtering
 * @param builder - CASL ability builder to apply abilities to
 */
function applyEmbeddedAbility(
  embedUser: CreateEmbedJwt,
  content: EmbedContent,
  embed: OssEmbed,
  externalId: string,
  builder: AbilityBuilder<MemberAbility>
): void;

Parameters:

  • embedUser: JWT user with content and permissions
  • content: Embedded content metadata (dashboard UUID, chart UUIDs, explores)
  • embed: Embed configuration with organization, project, and allowed content
  • externalId: External customer identifier for row-level filtering
  • builder: CASL AbilityBuilder to add permissions to

Grants permissions based on:

  • Dashboard or chart being embedded
  • JWT permissions (canExplore, canViewUnderlyingData, canExportCsv, canExportImages, canExportPagePdf, canDateZoom)
  • Organization and project context
  • External customer ID constraints

Embedded Content Types:

type EmbedContent = {
  dashboardUuid?: string;
  chartUuids: string[];
  explores: string[];
  type: 'dashboard' | 'chart';
};

type OssEmbed = {
  projectUuid: string;
  organization: Pick<Organization, 'organizationUuid' | 'name' | 'createdAt'>;
  encodedSecret: string;
  dashboardUuids: string[];
  allowAllDashboards: boolean;
  chartUuids: string[];
  allowAllCharts: boolean;
  createdAt: string;
  user: Pick<LightdashUser, 'userUuid' | 'firstName' | 'lastName'> | null;
};

type UserAccessControls = {
  userAttributes: UserAttributeValueMap;
  intrinsicUserAttributes: IntrinsicUserAttributes;
};

type EmbedAccess = {
  content: EmbedContent;
  filtering?: DashboardFilterInteractivityOptions;
  controls?: UserAccessControls;
  parameters?: ParameterInteractivityOptions;
};

Example:

import {
  getUserAbilityBuilder,
  applyEmbeddedAbility,
  JWT_HEADER_NAME,
} from '@lightdash/common';

// Create embedded user from JWT
const embedUser = {
  content: {
    type: 'dashboard',
    canExplore: true,
    canViewUnderlyingData: false,
    canExportCsv: true,
    canExportImages: true,
    canExportPagePdf: true,
    canDateZoom: true,
  },
};

// Create ability builder
const builder = getUserAbilityBuilder({
  user: embedUser,
  projectProfiles: [],
  permissionsConfig: { pat: { enabled: false, allowedOrgRoles: [] } },
});

// Apply embedded abilities
applyEmbeddedAbility(
  embedUser,
  dashboardContent,
  embedConfig,
  'customer-123',
  builder
);

const ability = builder.build();

// Check permissions
if (ability.can('export', 'Dashboard', { type: 'csv' })) {
  console.log('Can export to CSV');
}

Service Account Abilities

Service Account Scopes

Service accounts use hierarchical scope-based permissions for API access.

enum ServiceAccountScope {
  SCIM_MANAGE = 'scim:manage',
  ORG_READ = 'org:read',
  ORG_EDIT = 'org:edit',
  ORG_ADMIN = 'org:admin',
}

Scope Hierarchy:

  • SCIM_MANAGE: User provisioning via SCIM protocol
  • ORG_READ: View-only access to organization content (dashboards, charts, spaces, projects)
  • ORG_EDIT: ORG_READ + create/edit content in public spaces
  • ORG_ADMIN: ORG_EDIT + full admin capabilities (project management, user management, organization settings)

applyServiceAccountAbilities

Applies hierarchical abilities for service accounts based on their scopes.

/**
 * Apply service account abilities based on scopes
 * @param args - Service account configuration
 */
function applyServiceAccountAbilities(args: {
  organizationUuid: string;
  builder: AbilityBuilder<MemberAbility>;
  scopes: ServiceAccountScope[];
}): void;

Parameters:

  • organizationUuid: Organization the service account belongs to
  • builder: CASL AbilityBuilder to add permissions to (this function mutates the builder)
  • scopes: Array of service account scopes

This function mutates the builder parameter and returns nothing (void). Each scope grants specific abilities:

ORG_READ grants:

  • View dashboards, charts, spaces (including private with access)
  • View projects and organization details
  • View comments, tags, pinned items
  • Export to CSV
  • View underlying data and use semantic viewer
  • Explore and query data
  • Create scheduled deliveries
  • View enterprise features (metrics tree, spotlight, AI agent threads)

ORG_EDIT adds to ORG_READ:

  • Create and manage public spaces
  • Manage jobs, pinned items, scheduled deliveries
  • Edit dashboard comments, tags
  • Manage semantic viewer queries
  • Manage metrics tree

ORG_ADMIN adds to ORG_EDIT:

  • Full dashboard, chart, and space management
  • Create, update, and delete projects
  • Manage virtual views, custom SQL, SQL runner
  • Manage validations and compile projects
  • View analytics
  • Manage organization settings
  • Manage user profiles and groups
  • Manage enterprise features (content as code, AI agents, spotlight)

Example:

import {
  getUserAbilityBuilder,
  applyServiceAccountAbilities,
  ServiceAccountScope,
} from '@lightdash/common';

// Create service account with read-only access
const builder = getUserAbilityBuilder({
  user: serviceAccountUser,
  projectProfiles: [],
  permissionsConfig: { pat: { enabled: false, allowedOrgRoles: [] } },
});

applyServiceAccountAbilities({
  organizationUuid: 'org-uuid',
  builder,
  scopes: [ServiceAccountScope.ORG_READ],
});

const ability = builder.build();

// Can view but not modify
console.log('Can view:', ability.can('view', 'Dashboard')); // true
console.log('Can manage:', ability.can('manage', 'Dashboard')); // false

// For admin access
const adminBuilder = getUserAbilityBuilder({
  user: adminServiceAccount,
  projectProfiles: [],
  permissionsConfig: { pat: { enabled: false, allowedOrgRoles: [] } },
});

applyServiceAccountAbilities({
  organizationUuid: 'org-uuid',
  builder: adminBuilder,
  scopes: [ServiceAccountScope.ORG_ADMIN],
});

const adminAbility = adminBuilder.build();

// Has full access
console.log('Can manage:', adminAbility.can('manage', 'Dashboard')); // true
console.log('Can manage org:', adminAbility.can('manage', 'Organization')); // true

Usage Examples

Basic Permission Checking

import { defineUserAbility } from '@lightdash/common';

const ability = defineUserAbility(sessionUser, projectProfiles);

// Check if user can view a resource
if (ability.can('view', 'Dashboard')) {
  console.log('User can view dashboards');
}

// Check if user can create charts
if (ability.can('create', 'SavedChart')) {
  console.log('User can create saved charts');
}

// Check if user can update specific chart
if (ability.can('update', {
  subject: 'SavedChart',
  organizationUuid: 'org-uuid',
  projectUuid: 'project-uuid',
  access: [],
})) {
  console.log('User can update this chart');
}

// Check if user can manage organization
if (ability.can('manage', 'Organization')) {
  console.log('User is organization admin');
}

Role-Based Access

import {
  defineUserAbility,
  ProjectMemberRole,
  getAllScopesForRole,
} from '@lightdash/common';

// Get scopes for a role
const viewerScopes = getAllScopesForRole(ProjectMemberRole.VIEWER);
const editorScopes = getAllScopesForRole(ProjectMemberRole.EDITOR);

console.log('Viewer can:', viewerScopes);
console.log('Editor can:', editorScopes);

// Create ability for user with specific project role
const ability = defineUserAbility(
  sessionUser,
  [
    {
      projectUuid: 'project-1',
      role: ProjectMemberRole.EDITOR,
      userUuid: sessionUser.userUuid,
    },
    {
      projectUuid: 'project-2',
      role: ProjectMemberRole.VIEWER,
      userUuid: sessionUser.userUuid,
    },
  ]
);

// User has different abilities in different projects

Custom Role Scopes

import { defineUserAbility } from '@lightdash/common';

// Define custom role scopes
const customRoleScopes = {
  'custom-role-uuid': [
    'view:Project',
    'view:Dashboard',
    'view:SavedChart',
    'export:Csv',
  ],
};

const ability = defineUserAbility(
  sessionUser,
  [
    {
      projectUuid: 'project-1',
      role: 'custom-role-uuid' as ProjectMemberRole,
      userUuid: sessionUser.userUuid,
    },
  ],
  customRoleScopes
);

// User has custom role abilities

API Key Authorization

import { buildAccountHelpers } from '@lightdash/common';

const apiKeyAccount = {
  userId: 123,
  userUuid: 'user-uuid',
  organizationUuid: 'org-uuid',
  ability: defineUserAbility(user, projectProfiles),
};

const account = {
  ...apiKeyAccount,
  ...buildAccountHelpers(apiKeyAccount),
};

if (account.isPatUser()) {
  // Personal Access Token authentication
  console.log('Authenticated via PAT');
}

if (account.hasPrivilege('export', 'Dashboard')) {
  // Can export dashboard
}

Service Account with Limited Scopes

import {
  getUserAbilityBuilder,
  applyServiceAccountAbilities,
} from '@lightdash/common';

const builder = getUserAbilityBuilder({
  user: {
    userUuid: serviceAccountUuid,
    role: OrganizationMemberRole.MEMBER,
    organizationUuid: 'org-uuid',
    roleUuid: undefined,
  },
  projectProfiles: [],
  permissionsConfig: {
    pat: {
      enabled: false,
      allowedOrgRoles: [],
    },
  },
});

// Service account with read-only dashboard access
applyServiceAccountAbilities({
  organizationUuid: 'org-uuid',
  builder,
  scopes: [
    'view:Project',
    'view:Dashboard',
    'view:SavedChart',
  ],
});

const ability = builder.build();

// Can view but not modify
console.log('Can view:', ability.can('view', 'Dashboard')); // true
console.log('Can update:', ability.can('update', 'Dashboard')); // false

Embedded Dashboard Authorization

import {
  getUserAbilityBuilder,
  applyEmbeddedAbility,
  JWT_HEADER_NAME,
} from '@lightdash/common';

// Create embedded user
const embedUser = {
  userId: -1,
  userUuid: 'embed-user-uuid',
  email: 'embed@example.com',
  role: OrganizationMemberRole.VIEWER,
};

const builder = getUserAbilityBuilder({
  user: embedUser,
  projectProfiles: [],
  permissionsConfig: {
    pat: {
      enabled: false,
      allowedOrgRoles: [],
    },
  },
});

// Apply embedded abilities
applyEmbeddedAbility(
  embedUser,
  dashboard,
  {
    dashboardUuid: dashboard.uuid,
    secret: embedSecret,
    allowedFilters: ['date_range'],
  },
  externalCustomerId,
  builder
);

const ability = builder.build();

// Use in requests with JWT
fetch(`/api/v1/dashboards/${dashboard.uuid}`, {
  headers: {
    [JWT_HEADER_NAME]: jwtToken,
  },
});

JWT Authentication for Embedded Content

JWT (JSON Web Token) authentication enables secure embedding of Lightdash dashboards and charts in external applications. The JWT system provides fine-grained control over embedded content permissions.

JWT Header Constant

/**
 * HTTP header name for JWT tokens in embedded requests
 * Use this constant to ensure consistent header naming
 */
const JWT_HEADER_NAME = 'lightdash-embed-token';

Usage:

import { JWT_HEADER_NAME } from "@lightdash/common";

// Add JWT token to request headers
fetch('/api/v1/dashboards/123', {
  headers: {
    [JWT_HEADER_NAME]: jwtToken,
  },
});

applyEmbeddedAbility

Configures permissions for embedded dashboard or chart content using JWT authentication.

/**
 * Apply embedded content permissions to an ability builder
 * @param embedUser - JWT payload containing user and content information
 * @param content - Embedded content metadata (dashboard/chart UUIDs, explores)
 * @param embed - Embed configuration with organization and project context
 * @param externalId - External customer/user ID for tracking
 * @param builder - CASL ability builder to apply permissions to
 */
function applyEmbeddedAbility(
  embedUser: CreateEmbedJwt,
  content: EmbedContent,
  embed: OssEmbed,
  externalId: string,
  builder: AbilityBuilder<MemberAbility>
): void;

Types:

/**
 * JWT payload for embedded content
 */
interface CreateEmbedJwt {
  /** Content type and permissions */
  content: DashboardEmbedContent | ChartEmbedContent;
  /** External user identifier */
  externalId?: string;
}

/**
 * Dashboard embed configuration
 */
interface DashboardEmbedContent {
  type: 'dashboard';
  /** Dashboard UUID or slug */
  dashboardUuid?: string;
  dashboardSlug?: string;
  /** Allow date zoom interactions */
  canDateZoom?: boolean;
  /** Allow explore/drill-down */
  canExplore?: boolean;
  /** Allow viewing underlying data */
  canViewUnderlyingData?: boolean;
  /** Allow CSV export */
  canExportCsv?: boolean;
  /** Allow image export */
  canExportImages?: boolean;
  /** Allow PDF export (dashboard only) */
  canExportPagePdf?: boolean;
}

/**
 * Chart embed configuration
 */
interface ChartEmbedContent {
  type: 'chart';
  /** Array of chart UUIDs to allow */
  chartUuids: string[];
  /** Explores that can be accessed */
  explores: string[];
  /** Allow explore/drill-down */
  canExplore?: boolean;
  /** Allow viewing underlying data */
  canViewUnderlyingData?: boolean;
  /** Allow CSV export */
  canExportCsv?: boolean;
  /** Allow image export */
  canExportImages?: boolean;
}

/**
 * Embedded content metadata
 */
interface EmbedContent {
  type: 'dashboard' | 'chart';
  dashboardUuid?: string;
  chartUuids?: string[];
  explores?: string[];
}

/**
 * Embed configuration context
 */
interface OssEmbed {
  /** Organization context */
  organization: {
    organizationUuid: string;
  };
  /** Project UUID being embedded */
  projectUuid: string;
}

Dashboard Embedding Example:

import {
  applyEmbeddedAbility,
  getUserAbilityBuilder,
  JWT_HEADER_NAME,
  type CreateEmbedJwt,
  type EmbedContent,
  type OssEmbed
} from "@lightdash/common";

// Create JWT payload for dashboard embedding
const embedUser: CreateEmbedJwt = {
  content: {
    type: 'dashboard',
    dashboardUuid: 'abc123',
    canDateZoom: true,
    canExplore: true,
    canViewUnderlyingData: false,
    canExportCsv: true,
    canExportImages: true,
    canExportPagePdf: true,
  },
  externalId: 'customer-456',
};

// Content metadata
const content: EmbedContent = {
  type: 'dashboard',
  dashboardUuid: 'abc123',
  explores: ['orders', 'customers'],
};

// Embed configuration
const embed: OssEmbed = {
  organization: {
    organizationUuid: 'org-789',
  },
  projectUuid: 'project-xyz',
};

// Build ability with embedded permissions
const builder = getUserAbilityBuilder({
  user: {
    userUuid: 'embed-user',
    organizationUuid: 'org-789',
    role: undefined,
    roleUuid: undefined,
  },
  projectProfiles: [],
  permissionsConfig: {
    pat: { enabled: false, allowedOrgRoles: [] },
  },
});

applyEmbeddedAbility(embedUser, content, embed, 'customer-456', builder);

const ability = builder.build();

// Check what the embedded user can do
console.log('Can view dashboard:', ability.can('view', 'Dashboard'));
console.log('Can date zoom:', ability.can('view', { type: 'Dashboard', dateZoom: true }));
console.log('Can export CSV:', ability.can('export', { type: 'Dashboard', type: 'csv' }));

Chart Embedding Example:

import {
  applyEmbeddedAbility,
  getUserAbilityBuilder,
  type CreateEmbedJwt,
  type EmbedContent,
  type OssEmbed
} from "@lightdash/common";

// Create JWT payload for chart embedding
const embedUser: CreateEmbedJwt = {
  content: {
    type: 'chart',
    chartUuids: ['chart-1', 'chart-2'],
    explores: ['sales', 'inventory'],
    canExplore: false,              // Disable drill-down
    canViewUnderlyingData: false,   // Disable underlying data
    canExportCsv: true,             // Enable CSV export
    canExportImages: true,          // Enable image export
  },
  externalId: 'partner-company-123',
};

const content: EmbedContent = {
  type: 'chart',
  chartUuids: ['chart-1', 'chart-2'],
  explores: ['sales', 'inventory'],
};

const embed: OssEmbed = {
  organization: {
    organizationUuid: 'org-789',
  },
  projectUuid: 'project-xyz',
};

const builder = getUserAbilityBuilder({
  user: {
    userUuid: 'embed-chart-user',
    organizationUuid: 'org-789',
    role: undefined,
    roleUuid: undefined,
  },
  projectProfiles: [],
  permissionsConfig: {
    pat: { enabled: false, allowedOrgRoles: [] },
  },
});

applyEmbeddedAbility(embedUser, content, embed, 'partner-company-123', builder);

const ability = builder.build();

// The embedded chart has limited permissions
console.log('Can view chart:', ability.can('view', 'SavedChart'));
console.log('Can explore:', ability.can('view', 'Explore')); // false
console.log('Can export CSV:', ability.can('export', { type: 'SavedChart', type: 'csv' })); // true

JWT Ability Implementation Details

The JWT ability system applies different permissions based on content type:

Dashboard Abilities:

  • view permission on Dashboard with specific dashboardUuid
  • view permission on SavedChart (charts within the dashboard)
  • view permission on Project
  • Optional view permission on Dashboard with dateZoom: true (if enabled)
  • export permissions based on flags (images, CSV, PDF)
  • Optional view permission on Explore and UnderlyingData (if exploration enabled)

Chart Abilities:

  • view permission on SavedChart with specific chartUuids
  • view permission on Explore for specified explores
  • view permission on Project for specified explores
  • export permissions based on flags (images, CSV)
  • Optional view permission on UnderlyingData and Explore (if enabled)

Export Abilities:

Export permissions are automatically granted based on content flags:

// Dashboard exports
if (embedUser.content.canExportImages) {
  ability.can('export', { type: 'Dashboard', type: 'images' });
}
if (embedUser.content.canExportCsv) {
  ability.can('export', { type: 'Dashboard', type: 'csv' });
}
if (embedUser.content.type === 'dashboard' && embedUser.content.canExportPagePdf) {
  ability.can('export', { type: 'Dashboard', type: 'pdf' });
}

// Chart exports
if (embedUser.content.canExportImages) {
  ability.can('export', { type: 'SavedChart', type: 'images' });
}
if (embedUser.content.canExportCsv) {
  ability.can('export', { type: 'SavedChart', type: 'csv' });
}

Explore and Underlying Data:

// If canExplore or canViewUnderlyingData is enabled
if (embedUser.content.canExplore || embedUser.content.canViewUnderlyingData) {
  ability.can('view', 'UnderlyingData');
}

// If canExplore is enabled
if (embedUser.content.canExplore) {
  ability.can('view', 'Explore');
}

Complete JWT Embedding Workflow

import {
  applyEmbeddedAbility,
  getUserAbilityBuilder,
  JWT_HEADER_NAME,
  type CreateEmbedJwt,
  type EmbedContent,
  type OssEmbed
} from "@lightdash/common";
import jwt from 'jsonwebtoken';

// 1. Create JWT payload with permissions
const jwtPayload: CreateEmbedJwt = {
  content: {
    type: 'dashboard',
    dashboardUuid: 'dashboard-123',
    canDateZoom: true,
    canExplore: true,
    canViewUnderlyingData: true,
    canExportCsv: true,
    canExportImages: true,
    canExportPagePdf: true,
  },
  externalId: 'customer-abc',
};

// 2. Sign JWT token (server-side)
const embedSecret = process.env.LIGHTDASH_EMBED_SECRET;
const jwtToken = jwt.sign(jwtPayload, embedSecret, {
  expiresIn: '1h', // Token expiration
});

// 3. Client-side: Use token in embedded iframe
const iframeSrc = `https://lightdash.example.com/embed/dashboard-123?jwt=${jwtToken}`;

// 4. Server-side: Verify token and apply abilities
const decoded = jwt.verify(jwtToken, embedSecret) as CreateEmbedJwt;

const content: EmbedContent = {
  type: 'dashboard',
  dashboardUuid: decoded.content.dashboardUuid,
};

const embed: OssEmbed = {
  organization: { organizationUuid: 'org-123' },
  projectUuid: 'project-456',
};

const builder = getUserAbilityBuilder({
  user: {
    userUuid: `embed-${decoded.externalId}`,
    organizationUuid: 'org-123',
    role: undefined,
    roleUuid: undefined,
  },
  projectProfiles: [],
  permissionsConfig: {
    pat: { enabled: false, allowedOrgRoles: [] },
  },
});

applyEmbeddedAbility(decoded, content, embed, decoded.externalId || 'anonymous', builder);

const ability = builder.build();

// 5. Use ability to authorize requests
if (!ability.can('view', 'Dashboard')) {
  throw new Error('Unauthorized');
}

Security Considerations

Token Expiration:

  • Always set reasonable expiration times on JWT tokens (expiresIn option)
  • Regenerate tokens periodically for long-lived embeds

Secret Management:

  • Store embed secrets securely (environment variables, secret managers)
  • Never expose secrets in client-side code
  • Rotate secrets periodically

Permission Scope:

  • Grant minimum necessary permissions (principle of least privilege)
  • Disable exploration and underlying data access unless required
  • Limit chart access to specific UUIDs only

External ID Tracking:

  • Use externalId to track which customer/user accessed embedded content
  • Include in audit logs for compliance

Content Isolation:

  • Each JWT should reference specific dashboard/chart UUIDs
  • Avoid wildcard or broad access patterns

Checking Multiple Permissions

import { defineUserAbility } from '@lightdash/common';

const ability = defineUserAbility(sessionUser, projectProfiles);

// Check multiple permissions
const permissions = {
  canViewDashboards: ability.can('view', 'Dashboard'),
  canCreateDashboards: ability.can('create', 'Dashboard'),
  canUpdateDashboards: ability.can('update', 'Dashboard'),
  canDeleteDashboards: ability.can('delete', 'Dashboard'),
  canExportData: ability.can('export', 'ExportCsv'),
  canManageSpaces: ability.can('manage', 'Space'),
  canRunUnderlying: ability.can('view', 'UnderlyingData'),
};

console.log('User permissions:', permissions);

// Show/hide UI elements based on permissions
if (permissions.canCreateDashboards) {
  // Show "Create Dashboard" button
}

Organization-Level Permissions

import {
  defineUserAbility,
  OrganizationMemberRole,
} from '@lightdash/common';

const adminUser: SessionUser = {
  // ... user properties
  role: OrganizationMemberRole.ADMIN,
};

const ability = defineUserAbility(adminUser);

// Organization admins have elevated privileges
console.log('Can manage org:', ability.can('manage', 'Organization'));
console.log('Can manage users:', ability.can('manage', 'OrganizationMemberProfile'));
console.log('Can manage projects:', ability.can('manage', 'Project'));
console.log('Can manage groups:', ability.can('manage', 'Group'));