CASL-based authorization system with role-based access control, scopes, and abilities for users, service accounts, and embedded contexts.
Lightdash uses CASL (an isomorphic authorization library) to manage permissions across the platform. The authorization system supports:
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.
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>;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:
roleUuid is optional and only used for custom rolesroleUuid in each profile is optional and only used for custom rolesReturns 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
}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();Functions for working with project member roles and their associated scopes. These utilities help map roles to permissions and determine role types.
Gets all scopes (including inherited) for a project member role.
function getAllScopesForRole(role: ProjectMemberRole): string[];Parameters:
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:
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 expansionGets only non-enterprise scopes for a role, filtering out enterprise-only features.
function getNonEnterpriseScopesForRole(role: ProjectMemberRole): string[];Parameters:
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:MetricsTreemanage:MetricsTreeview:SpotlightTableConfigmanage:SpotlightTableConfigview:AiAgentview:AiAgentThreadcreate:AiAgentThreadmanage:AiAgentmanage:AiAgentThreadmanage:ContentAsCodemanage:PersonalAccessTokenExample:
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
);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,
}));Type guard that checks if a role UUID is a built-in system role.
function isSystemRole(roleUuid: string): roleUuid is ProjectMemberRole;Parameters:
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!)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 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}`);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')); // falseThe 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>;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:
Returns an object with the following helper methods:
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');
}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:
Returns the appropriate account type (SessionAccount, ApiKeyAccount, AnonymousAccount, or ServiceAcctAccount) with:
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');
}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;
}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];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:
Grants permissions based on:
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 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:
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:
This function mutates the builder parameter and returns nothing (void). Each scope grants specific abilities:
ORG_READ grants:
ORG_EDIT adds to ORG_READ:
ORG_ADMIN adds to ORG_EDIT:
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')); // trueimport { 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');
}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 projectsimport { 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 abilitiesimport { 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
}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')); // falseimport {
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 (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.
/**
* 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,
},
});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' })); // trueThe JWT ability system applies different permissions based on content type:
Dashboard Abilities:
view permission on Dashboard with specific dashboardUuidview permission on SavedChart (charts within the dashboard)view permission on Projectview permission on Dashboard with dateZoom: true (if enabled)export permissions based on flags (images, CSV, PDF)view permission on Explore and UnderlyingData (if exploration enabled)Chart Abilities:
view permission on SavedChart with specific chartUuidsview permission on Explore for specified exploresview permission on Project for specified exploresexport permissions based on flags (images, CSV)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');
}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');
}Token Expiration:
expiresIn option)Secret Management:
Permission Scope:
External ID Tracking:
externalId to track which customer/user accessed embedded contentContent Isolation:
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
}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'));