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

roles.mddocs/api/utilities/specialized/

Role Utilities

Functions for converting and managing roles across organization, project, and space contexts.

Capabilities

This module provides the following functionality:

Role Conversion Functions

/**
 * Convert an organization role to the equivalent project role
 * @param role - Organization member role
 * @returns Corresponding project member role
 */
function convertOrganizationRoleToProjectRole(
  role: OrganizationMemberRole
): ProjectMemberRole;

/**
 * Convert a project role to the equivalent organization role
 * @param role - Project member role
 * @returns Corresponding organization member role
 */
function convertProjectRoleToOrganizationRole(
  role: ProjectMemberRole
): OrganizationMemberRole;

/**
 * Convert a project role to the equivalent space role
 * @param role - Project member role
 * @returns Corresponding space member role
 */
function convertProjectRoleToSpaceRole(
  role: ProjectMemberRole
): SpaceMemberRole;

/**
 * Convert a space role to the equivalent project role
 * @param role - Space member role
 * @returns Corresponding project member role
 */
function convertSpaceRoleToProjectRole(
  role: SpaceMemberRole
): ProjectMemberRole;

/**
 * Get the highest role from multiple inherited roles
 * @param inheritedRoles - Array of inherited roles from different sources
 * @returns The highest role or undefined
 */
function getHighestProjectRole(
  inheritedRoles: Array<OrganizationRole | ProjectRole | GroupRole | SpaceGroupAccessRole>
): InheritedProjectRole | undefined;

Examples

Basic Role Conversion

import {
  convertOrganizationRoleToProjectRole,
  convertProjectRoleToOrganizationRole,
  convertProjectRoleToSpaceRole,
  convertSpaceRoleToProjectRole,
  OrganizationMemberRole,
  ProjectMemberRole,
  SpaceMemberRole,
} from '@lightdash/common';

// Convert organization role to project role
const projectRole = convertOrganizationRoleToProjectRole(
  OrganizationMemberRole.EDITOR
);
// Returns: ProjectMemberRole.EDITOR

// Convert project role to organization role
const orgRole = convertProjectRoleToOrganizationRole(
  ProjectMemberRole.VIEWER
);
// Returns: OrganizationMemberRole.VIEWER

// Convert project role to space role
const spaceRole = convertProjectRoleToSpaceRole(
  ProjectMemberRole.DEVELOPER
);
// Returns: SpaceMemberRole.EDITOR

// Convert space role to project role
const projectRole2 = convertSpaceRoleToProjectRole(
  SpaceMemberRole.VIEWER
);
// Returns: ProjectMemberRole.VIEWER

Role Hierarchy

import {
  getHighestProjectRole,
  type OrganizationRole,
  type ProjectRole,
  type GroupRole,
} from '@lightdash/common';

// Get highest role from multiple sources
const highestRole = getHighestProjectRole([
  { type: 'organization', role: ProjectMemberRole.VIEWER },
  { type: 'project', role: ProjectMemberRole.EDITOR },
  { type: 'group', role: ProjectMemberRole.ADMIN },
]);
// Returns: { type: 'group', role: ProjectMemberRole.ADMIN }

// User with only organization role
const orgOnlyRole = getHighestProjectRole([
  { type: 'organization', role: ProjectMemberRole.DEVELOPER },
]);
// Returns: { type: 'organization', role: ProjectMemberRole.DEVELOPER }

// No roles
const noRole = getHighestProjectRole([]);
// Returns: undefined

Permission Checking

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

function getUserProjectPermissions(user: User, projectId: string) {
  const roles = [];

  // Add organization role
  if (user.organizationRole) {
    const projectRole = convertOrganizationRoleToProjectRole(
      user.organizationRole
    );
    roles.push({
      type: 'organization',
      role: projectRole,
    });
  }

  // Add direct project role
  const projectMembership = user.projectMemberships.find(
    m => m.projectId === projectId
  );
  if (projectMembership) {
    roles.push({
      type: 'project',
      role: projectMembership.role,
    });
  }

  // Add group roles
  for (const group of user.groups) {
    const groupProjectAccess = group.projectAccess.find(
      a => a.projectId === projectId
    );
    if (groupProjectAccess) {
      roles.push({
        type: 'group',
        role: groupProjectAccess.role,
      });
    }
  }

  // Get highest role
  return getHighestProjectRole(roles);
}

// Usage
const userRole = getUserProjectPermissions(currentUser, 'project-123');
if (userRole?.role === ProjectMemberRole.ADMIN) {
  // User has admin permissions
}

Access Control

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

function canEditProject(user: User, project: Project): boolean {
  const roles = getUserInheritedRoles(user, project);
  const highestRole = getHighestProjectRole(roles);

  if (!highestRole) {
    return false;
  }

  // Editor and above can edit
  return [
    ProjectMemberRole.EDITOR,
    ProjectMemberRole.DEVELOPER,
    ProjectMemberRole.ADMIN,
  ].includes(highestRole.role);
}

function canDeleteProject(user: User, project: Project): boolean {
  const roles = getUserInheritedRoles(user, project);
  const highestRole = getHighestProjectRole(roles);

  // Only admins can delete
  return highestRole?.role === ProjectMemberRole.ADMIN;
}

Role Migration

import {
  convertOrganizationRoleToProjectRole,
  convertProjectRoleToSpaceRole,
} from '@lightdash/common';

async function migrateUserRoles() {
  const users = await database.users.findMany();

  for (const user of users) {
    // Convert organization role to project role
    const projectRole = convertOrganizationRoleToProjectRole(
      user.organizationRole
    );

    // Convert project role to space role
    const spaceRole = convertProjectRoleToSpaceRole(projectRole);

    // Update user's default space role
    await database.users.update(user.id, {
      defaultSpaceRole: spaceRole,
    });
  }
}

API Authorization

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

// Middleware to check project access
function requireProjectRole(minimumRole: ProjectMemberRole) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const projectId = req.params.projectId;
    const user = req.user;

    const roles = getUserInheritedRoles(user, projectId);
    const highestRole = getHighestProjectRole(roles);

    if (!highestRole) {
      return res.status(403).json({ error: 'No access to this project' });
    }

    // Check if user has sufficient role
    const roleHierarchy = [
      ProjectMemberRole.VIEWER,
      ProjectMemberRole.INTERACTIVE_VIEWER,
      ProjectMemberRole.EDITOR,
      ProjectMemberRole.DEVELOPER,
      ProjectMemberRole.ADMIN,
    ];

    const userRoleIndex = roleHierarchy.indexOf(highestRole.role);
    const requiredRoleIndex = roleHierarchy.indexOf(minimumRole);

    if (userRoleIndex < requiredRoleIndex) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }

    next();
  };
}

// Usage in routes
app.get(
  '/api/projects/:projectId',
  requireProjectRole(ProjectMemberRole.VIEWER)
);

app.put(
  '/api/projects/:projectId',
  requireProjectRole(ProjectMemberRole.EDITOR)
);

app.delete(
  '/api/projects/:projectId',
  requireProjectRole(ProjectMemberRole.ADMIN)
);

Role Display

import {
  getHighestProjectRole,
  type InheritedProjectRole,
} from '@lightdash/common';

function getRoleDisplayInfo(inheritedRole: InheritedProjectRole) {
  const sourceLabels = {
    organization: 'Organization',
    project: 'Direct',
    group: 'Group',
    space: 'Space',
  };

  const roleLabels = {
    [ProjectMemberRole.VIEWER]: 'Viewer',
    [ProjectMemberRole.INTERACTIVE_VIEWER]: 'Interactive Viewer',
    [ProjectMemberRole.EDITOR]: 'Editor',
    [ProjectMemberRole.DEVELOPER]: 'Developer',
    [ProjectMemberRole.ADMIN]: 'Admin',
  };

  return {
    role: roleLabels[inheritedRole.role],
    source: sourceLabels[inheritedRole.type],
    description: `${roleLabels[inheritedRole.role]} via ${sourceLabels[inheritedRole.type]}`,
  };
}

// Usage in UI
const userRoles = getUserInheritedRoles(user, project);
const highestRole = getHighestProjectRole(userRoles);

if (highestRole) {
  const displayInfo = getRoleDisplayInfo(highestRole);
  console.log(displayInfo.description);
  // Output: "Admin via Group"
}

Group Role Assignment

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

async function assignGroupToSpace(
  groupId: string,
  spaceId: string,
  projectRole: ProjectMemberRole
) {
  // Convert project role to appropriate space role
  const spaceRole = convertProjectRoleToSpaceRole(projectRole);

  await database.spaceGroupAccess.create({
    groupId,
    spaceId,
    role: spaceRole,
  });

  console.log(`Group assigned to space with role: ${spaceRole}`);
}

Testing

import {
  convertOrganizationRoleToProjectRole,
  convertSpaceRoleToProjectRole,
  getHighestProjectRole,
  OrganizationMemberRole,
  ProjectMemberRole,
  SpaceMemberRole,
} from '@lightdash/common';

describe('Role utilities', () => {
  describe('role conversion', () => {
    it('should convert organization role to project role', () => {
      const role = convertOrganizationRoleToProjectRole(
        OrganizationMemberRole.EDITOR
      );
      expect(role).toBe(ProjectMemberRole.EDITOR);
    });

    it('should convert space role to project role', () => {
      const role = convertSpaceRoleToProjectRole(SpaceMemberRole.VIEWER);
      expect(role).toBe(ProjectMemberRole.VIEWER);
    });
  });

  describe('getHighestProjectRole', () => {
    it('should return highest role', () => {
      const roles = [
        { type: 'organization', role: ProjectMemberRole.VIEWER },
        { type: 'project', role: ProjectMemberRole.ADMIN },
      ];

      const highest = getHighestProjectRole(roles);
      expect(highest?.role).toBe(ProjectMemberRole.ADMIN);
    });

    it('should return undefined for empty array', () => {
      const highest = getHighestProjectRole([]);
      expect(highest).toBeUndefined();
    });
  });
});

Role Hierarchy

Project Roles (Lowest to Highest)

  1. Viewer: Read-only access
  2. Interactive Viewer: Can interact with charts
  3. Editor: Can create and edit content
  4. Developer: Can manage models and schemas
  5. Admin: Full control

Organization Roles

  • Member: Organization member
  • Viewer: View organization content
  • Interactive Viewer: Interactive organization access
  • Editor: Edit organization content
  • Developer: Develop organization projects
  • Admin: Full organization control

Space Roles

  • Viewer: View space content
  • Editor: Edit space content
  • Admin: Manage space settings

Role Conversion Mapping

Organization RoleProject RoleSpace Role
AdminAdminAdmin
DeveloperDeveloperEditor
EditorEditorEditor
Interactive ViewerInteractive ViewerViewer
ViewerViewerViewer
MemberViewerViewer

Use Cases

  • Permission Checking: Determine user permissions for projects and spaces
  • Role Migration: Convert roles when moving users between contexts
  • Access Control: Implement role-based access control
  • UI Display: Show user roles and sources in UI
  • API Authorization: Enforce role requirements in API endpoints
  • Group Management: Assign groups to projects and spaces
  • Inheritance: Calculate effective permissions from multiple sources

Related Utilities

  • Permission Types: See types documentation for role enums
  • User Management: Integration with user management systems
  • Access Control: Part of broader authorization framework