This guide shows you how to implement role-based access control using the CASL-based authorization system. For detailed API documentation, see Authorization API.
Lightdash uses CASL (an isomorphic authorization library) for permissions:
import { defineUserAbility } from "@lightdash/common";
const ability = defineUserAbility(sessionUser, projectProfiles);
// Check permissions
if (ability.can("view", "Dashboard")) {
console.log("User can view dashboards");
}
if (ability.can("create", "SavedChart")) {
console.log("User can create charts");
}
if (ability.can("manage", "Organization")) {
console.log("User is organization admin");
}import {
defineUserAbility,
OrganizationMemberRole,
type LightdashUser,
} from "@lightdash/common";
const adminUser: LightdashUser = {
userUuid: "user-123",
organizationUuid: "org-456",
role: OrganizationMemberRole.ADMIN,
// ... other properties
};
const ability = defineUserAbility(adminUser, []);
// Organization admins have elevated privileges
console.log(ability.can("manage", "Organization")); // true
console.log(ability.can("manage", "OrganizationMemberProfile")); // true
console.log(ability.can("manage", "Project")); // trueimport {
defineUserAbility,
ProjectMemberRole,
type ProjectMemberProfile,
} from "@lightdash/common";
const projectProfiles: ProjectMemberProfile[] = [
{
projectUuid: "project-1",
role: ProjectMemberRole.EDITOR,
userUuid: "user-123",
},
{
projectUuid: "project-2",
role: ProjectMemberRole.VIEWER,
userUuid: "user-123",
},
];
const ability = defineUserAbility(user, projectProfiles);
// User has different abilities in different projectsenum OrganizationMemberRole {
ADMIN = "admin",
MEMBER = "member",
VIEWER = "viewer",
}enum ProjectMemberRole {
ADMIN = "admin",
DEVELOPER = "developer",
EDITOR = "editor",
INTERACTIVE_VIEWER = "interactive_viewer",
VIEWER = "viewer",
}Role Hierarchy (most to least permissions):
import { getAllScopesForRole, ProjectMemberRole } from "@lightdash/common";
// Get all scopes for a role
const editorScopes = getAllScopesForRole(ProjectMemberRole.EDITOR);
console.log(`Editor has ${editorScopes.length} scopes`);
// Check if role has specific scope
if (editorScopes.includes("manage:Dashboard")) {
console.log("Editors can manage dashboards");
}const ability = defineUserAbility(user, projectProfiles);
// Simple checks
ability.can("view", "Dashboard") // Can view dashboards?
ability.can("create", "SavedChart") // Can create charts?
ability.can("update", "Space") // Can update spaces?
ability.can("delete", "Project") // Can delete projects?
ability.can("export", "ExportCsv") // Can export to CSV?
ability.can("manage", "Organization") // Can manage organization?Check permissions on specific resources:
// Check if user can update a specific chart
if (ability.can("update", {
subject: "SavedChart",
organizationUuid: "org-456",
projectUuid: "project-1",
access: [],
})) {
console.log("User can update this chart");
}
// Check project access
if (ability.can("view", {
type: "Project",
uuid: "project-1",
})) {
console.log("User can view project-1");
}Show/hide UI elements based on permissions:
function DashboardActions({ ability }) {
return (
<div>
{ability.can("create", "Dashboard") && (
<button>Create Dashboard</button>
)}
{ability.can("update", "Dashboard") && (
<button>Edit Dashboard</button>
)}
{ability.can("delete", "Dashboard") && (
<button>Delete Dashboard</button>
)}
{ability.can("export", "DashboardPdf") && (
<button>Export to PDF</button>
)}
</div>
);
}import { defineUserAbility } from "@lightdash/common";
// Define custom role scopes
const customRoleScopes = {
"custom-role-uuid": [
"view:Project",
"view:Dashboard",
"view:SavedChart",
"export:DashboardCsv",
"view:UnderlyingData",
],
};
const ability = defineUserAbility(
user,
[{
projectUuid: "project-1",
role: "custom-role-uuid" as ProjectMemberRole,
userUuid: user.userUuid,
roleUuid: "custom-role-uuid",
}],
customRoleScopes
);
// User has custom role abilitiesScopes are granular permissions in the format action:Subject:
// View permissions
"view:Dashboard"
"view:SavedChart"
"view:Project"
// Manage permissions
"manage:Dashboard"
"manage:SavedChart"
"manage:Space"
// Export permissions
"export:DashboardCsv"
"export:DashboardPdf"
"export:DashboardImage"
// Data access
"view:UnderlyingData"
"manage:SqlRunner"
"manage:CustomSql"Some scopes have modifiers for conditional access:
// Space management
"manage:Space" // Manage all spaces
"manage:Space@public" // Manage public spaces only
"manage:Space@assigned" // Manage assigned spaces only
// Self-access only
"view:Job@self" // View own jobs only
"delete:Project@self" // Delete own projects onlyService accounts use scope-based permissions for API access.
import {
applyServiceAccountAbilities,
ServiceAccountScope,
getUserAbilityBuilder,
} from "@lightdash/common";
const builder = getUserAbilityBuilder({
user: serviceAccount,
projectProfiles: [],
permissionsConfig: { pat: { enabled: false, allowedOrgRoles: [] } },
});
// Read-only access
applyServiceAccountAbilities({
organizationUuid: "org-uuid",
builder,
scopes: [ServiceAccountScope.ORG_READ],
});
const ability = builder.build();
console.log(ability.can("view", "Dashboard")); // true
console.log(ability.can("manage", "Dashboard")); // falseenum ServiceAccountScope {
ORG_READ = "org:read", // View-only access
ORG_EDIT = "org:edit", // Create/edit in public spaces
ORG_ADMIN = "org:admin", // Full admin access
SCIM_MANAGE = "scim:manage", // User provisioning
}Control access to embedded dashboards and charts:
import {
applyEmbeddedAbility,
getUserAbilityBuilder,
JWT_HEADER_NAME,
} from "@lightdash/common";
const embedUser = {
content: {
type: "dashboard",
canExplore: true,
canViewUnderlyingData: false,
canExportCsv: true,
canExportImages: true,
canExportPagePdf: true,
canDateZoom: true,
},
};
const builder = getUserAbilityBuilder({
user: embedUser,
projectProfiles: [],
permissionsConfig: { pat: { enabled: false, allowedOrgRoles: [] } },
});
applyEmbeddedAbility(
embedUser,
dashboardContent,
embedConfig,
"customer-123",
builder
);
const ability = builder.build();Batch permission checks for UI:
import { defineUserAbility } from "@lightdash/common";
const ability = defineUserAbility(user, projectProfiles);
// Check multiple permissions at once
const permissions = {
// Content permissions
canViewDashboards: ability.can("view", "Dashboard"),
canCreateDashboards: ability.can("create", "Dashboard"),
canUpdateDashboards: ability.can("update", "Dashboard"),
canDeleteDashboards: ability.can("delete", "Dashboard"),
// Export permissions
canExportCsv: ability.can("export", "ExportCsv"),
canExportPdf: ability.can("export", "DashboardPdf"),
canExportImages: ability.can("export", "DashboardImage"),
// Data access
canViewUnderlyingData: ability.can("view", "UnderlyingData"),
canRunCustomSql: ability.can("manage", "CustomSql"),
// Admin permissions
canManageSpaces: ability.can("manage", "Space"),
canManageProject: ability.can("manage", "Project"),
canManageOrg: ability.can("manage", "Organization"),
};
console.log("User permissions:", permissions);// 1. Check if user can create
if (!ability.can("create", "Dashboard")) {
throw new Error("Unauthorized: Cannot create dashboard");
}
// 2. Create the resource
const dashboard = await createDashboard(data);
// 3. Check if user can view the created resource
if (ability.can("view", dashboard)) {
return dashboard;
}// 1. Load resource
const dashboard = await getDashboard(uuid);
// 2. Check update permission
if (!ability.can("update", dashboard)) {
throw new Error("Unauthorized: Cannot update dashboard");
}
// 3. Update resource
await updateDashboard(uuid, changes);// 1. Check delete permission
if (!ability.can("delete", {
subject: "Dashboard",
uuid: dashboardUuid,
})) {
throw new Error("Unauthorized: Cannot delete dashboard");
}
// 2. Delete resource
await deleteDashboard(dashboardUuid);Always check permissions before performing operations:
// Good
if (!ability.can("update", "Dashboard")) {
return res.status(403).json({ error: "Forbidden" });
}
// ... proceed with update
// Avoid
// ... perform update, then check permissionsBe specific about what you're checking:
// Good
ability.can("update", "Dashboard")
ability.can("export", "DashboardCsv")
// Less clear
ability.can("manage", "Dashboard") // Too broadCreate abilities once per request:
// In middleware
req.ability = defineUserAbility(req.user, req.projectProfiles);
// In route handlers
if (req.ability.can("view", "Dashboard")) {
// ...
}Provide clear error messages:
if (!ability.can("create", "Dashboard")) {
throw new Error(
"You don't have permission to create dashboards. " +
"Contact your organization admin to request access."
);
}