Skip to content

Access Control (ACL)

This document provides a comprehensive guide to implementing access control rules in Jodit Connector Node.js.

Table of Contents

Overview

Access Control Lists (ACL) define fine-grained permissions based on user role, path, and file extensions. Rules can be static (defined at startup) or dynamic (loaded from database, API, or computed at runtime).

Basic Rules

Rule Structure

interface AccessControlRule {
  role?: string;           // User role or '*' for all
  path?: string;           // Path restriction
  extensions?: string[];   // Allowed file extensions
  [action: string]: boolean | Function;  // Action permissions
}

Example Configuration

const config = {
  defaultRole: 'guest',
  accessControl: [
    // General rules first (less specific)
    {
      role: 'guest',
      FILES: true,
      FILE_UPLOAD: false,
      FILE_REMOVE: false
    },
    // Specific rules override general rules
    {
      role: 'guest',
      path: '/private',
      FILES: false // Deny access to /private folder
    },
    {
      role: 'user',
      FILES: true,
      FILE_UPLOAD: true,
      FILE_REMOVE: false,
      FOLDER_CREATE: true
    },
    {
      role: 'admin',
      FILES: true,
      FILE_UPLOAD: true,
      FILE_REMOVE: true,
      FOLDER_CREATE: true,
      FOLDER_REMOVE: true
    },
    {
      role: 'editor',
      extensions: ['jpg', 'png', 'gif'], // Only images
      FILE_UPLOAD: true,
      FILE_REMOVE: false
    },
    {
      role: '*', // Wildcard - matches all roles
      path: '/public',
      FILES: true
    }
  ]
};

Available Actions

Action Description
FILES List files
FILE_UPLOAD Upload files
FILE_UPLOAD_REMOTE Upload from URL
FILE_REMOVE Delete files
FILE_MOVE Move files
FILE_RENAME Rename files
FILE_DOWNLOAD Download files
FOLDERS List folders
FOLDER_CREATE Create folders
FOLDER_REMOVE Delete folders
FOLDER_MOVE Move folders
FOLDER_RENAME Rename folders
IMAGE_RESIZE Resize images
IMAGE_CROP Crop images
GENERATE_PDF Generate PDF
GENERATE_DOCX Generate DOCX

Rule Matching

  • Order matters: Rules are processed in order (general → specific)
  • Later rules override earlier ones for the same role/action
  • Role matching: Exact match or wildcard ('*')
  • Path matching: Checks if request path starts with rule path
  • Extension matching: Filters by file extension

Dynamic Rules with Functions

For complex logic, use functions instead of boolean values:

{
  role: 'editor',
  extensions: (action, rule, path, ext) => {
    // Custom logic for allowed extensions
    if (path.startsWith('/images')) {
      return ['jpg', 'png', 'gif'];
    }
    if (path.startsWith('/documents')) {
      return ['pdf', 'doc', 'docx'];
    }
    return ['*']; // Allow all in other folders
  },
  FILE_UPLOAD: (action, rule, path, ext) => {
    // Custom logic for upload permission
    return path !== '/protected';
  },
  FILE_REMOVE: (action, rule, path, ext) => {
    // Only allow removing own files
    return path.startsWith(`/users/${getUserId(rule)}`);
  }
}

Dynamic Access Control

Access control rules can be loaded dynamically from external sources like databases, APIs, or cache systems.

Static vs Dynamic Rules

Static rules (array):

{
  accessControl: [
    { role: 'guest', FILES: true, FILE_UPLOAD: false },
    { role: 'admin', FILES: true, FILE_UPLOAD: true }
  ]
}
  • ✅ Simple and fast
  • ✅ No database calls
  • ❌ Fixed at startup
  • ❌ Requires restart to update

Dynamic rules (async function):

{
  accessControl: async () => {
    const rules = await loadFromDatabase();
    return rules;
  }
}
  • ✅ Fresh rules on every check
  • ✅ No restart needed for updates
  • ✅ Centralized rule management
  • ⚠️ Adds latency (use caching!)

Loading Rules from Database

import { start } from 'jodit-nodejs';
import { database } from './database';

await start({
  port: 8081,
  config: {
    defaultRole: 'guest',
    // Load ACL rules from database on every permission check
    accessControl: async () => {
      const rules = await database.query(`
        SELECT role, action, allowed
        FROM acl_rules
        WHERE active = true
        ORDER BY priority
      `);

      // Transform database rows to AccessControlRule format
      const rulesByRole: Record<string, any> = {};

      for (const row of rules) {
        if (!rulesByRole[row.role]) {
          rulesByRole[row.role] = { role: row.role };
        }
        rulesByRole[row.role][row.action] = row.allowed;
      }

      return Object.values(rulesByRole);
    }
  }
});

Caching for Performance

Loading rules from database on every permission check can be slow. Add caching:

import { start } from 'jodit-nodejs';
import { database } from './database';

// Simple in-memory cache
let cachedRules: AccessControlRule[] | null = null;
let cacheExpiry = 0;
const CACHE_TTL = 60000; // 1 minute

async function loadACLRules(): Promise<AccessControlRule[]> {
  const now = Date.now();

  // Return cached rules if still valid
  if (cachedRules && now < cacheExpiry) {
    return cachedRules;
  }

  // Load fresh rules from database
  const rules = await database.query(`
    SELECT role, action, allowed
    FROM acl_rules
    WHERE active = true
  `);

  const rulesByRole: Record<string, any> = {};
  for (const row of rules) {
    if (!rulesByRole[row.role]) {
      rulesByRole[row.role] = { role: row.role };
    }
    rulesByRole[row.role][row.action] = row.allowed;
  }

  cachedRules = Object.values(rulesByRole);
  cacheExpiry = now + CACHE_TTL;

  return cachedRules;
}

await start({
  port: 8081,
  config: {
    defaultRole: 'guest',
    accessControl: loadACLRules
  }
});

Loading Rules from Redis

import { start } from 'jodit-nodejs';
import Redis from 'ioredis';

const redis = new Redis();

async function loadACLFromRedis(): Promise<AccessControlRule[]> {
  // Try to get cached rules
  const cached = await redis.get('acl:rules');

  if (cached) {
    return JSON.parse(cached);
  }

  // Load from primary source (database)
  const rules = await loadFromDatabase();

  // Cache for 5 minutes
  await redis.setex('acl:rules', 300, JSON.stringify(rules));

  return rules;
}

await start({
  port: 8081,
  config: {
    defaultRole: 'guest',
    accessControl: loadACLFromRedis
  }
});

Loading Rules from API

import { start } from 'jodit-nodejs';
import fetch from 'node-fetch';

async function loadACLFromAPI(): Promise<AccessControlRule[]> {
  const response = await fetch('https://api.example.com/acl/rules', {
    headers: {
      'Authorization': `Bearer ${process.env.API_TOKEN}`
    }
  });

  if (!response.ok) {
    // Fallback to safe defaults on error
    return [
      { role: 'guest', FILES: true, FILE_UPLOAD: false }
    ];
  }

  const data = await response.json();
  return data.rules;
}

await start({
  port: 8081,
  config: {
    defaultRole: 'guest',
    accessControl: loadACLFromAPI
  }
});

Synchronous Function (Computed Rules)

For rules that depend on application state but don't need async operations:

import { start } from 'jodit-nodejs';

// Rules computed from environment
function getACLRules(): AccessControlRule[] {
  const isProduction = process.env.NODE_ENV === 'production';

  if (isProduction) {
    // Strict rules in production
    return [
      { role: 'guest', FILES: true, FILE_UPLOAD: false },
      { role: 'user', FILES: true, FILE_UPLOAD: true, FILE_REMOVE: false },
      { role: 'admin', FILES: true, FILE_UPLOAD: true, FILE_REMOVE: true }
    ];
  } else {
    // Relaxed rules in development
    return [
      { role: '*', FILES: true, FILE_UPLOAD: true, FILE_REMOVE: true }
    ];
  }
}

await start({
  port: 8081,
  config: {
    defaultRole: 'guest',
    accessControl: getACLRules  // Sync function
  }
});

Best Practices for Dynamic Rules

  1. Always implement caching for database/API calls to avoid performance issues
  2. Set appropriate TTL (cache lifetime) based on how often rules change
  3. Provide fallback rules in case loading fails
  4. Monitor performance - async rule loading adds latency to every request
  5. Use sync functions when rules depend only on application state
  6. Log rule changes for audit and debugging
  7. Test failure scenarios (database down, API timeout, etc.)

Complete Example with Error Handling

import { start } from 'jodit-nodejs';
import { database } from './database';
import { logger } from './logger';

let cachedRules: AccessControlRule[] = [
  // Safe defaults as fallback
  { role: 'guest', FILES: true, FILE_UPLOAD: false }
];
let cacheExpiry = 0;

async function loadACL(): Promise<AccessControlRule[]> {
  const now = Date.now();

  // Return cached rules if valid
  if (now < cacheExpiry) {
    return cachedRules;
  }

  try {
    // Load fresh rules from database
    const rules = await database.query('SELECT * FROM acl_rules');

    const transformed = transformRules(rules);

    // Update cache
    cachedRules = transformed;
    cacheExpiry = now + 60000; // 1 minute

    logger.info(`Loaded ${transformed.length} ACL rules from database`);

    return transformed;
  } catch (error) {
    logger.error('Failed to load ACL rules from database:', error);

    // Return last successful cache or defaults
    return cachedRules;
  }
}

await start({
  port: 8081,
  config: {
    defaultRole: 'guest',
    accessControl: loadACL
  }
});

Advanced Examples

Multi-source Permissions

Different permissions for different file sources:

const config = {
  defaultRole: 'guest',
  sources: {
    public: {
      name: 'public',
      title: 'Public Files',
      root: '/var/www/public',
      baseurl: 'https://cdn.example.com/public/'
    },
    private: {
      name: 'private',
      title: 'Private Files',
      root: '/var/www/private',
      baseurl: 'https://cdn.example.com/private/'
    }
  },
  accessControl: [
    // Guest can view public files only
    {
      role: 'guest',
      FILES: true,
      FILE_UPLOAD: false
    },
    // User can upload to public, view private
    {
      role: 'user',
      FILES: true,
      FILE_UPLOAD: true,
      FILE_REMOVE: false
    },
    // Admin has full access
    {
      role: 'admin',
      FILES: true,
      FILE_UPLOAD: true,
      FILE_REMOVE: true,
      FOLDER_CREATE: true
    }
  ]
};

Role-based File Filtering

Show different files to different roles:

const config = {
  accessControl: [
    {
      role: 'guest',
      path: '/public',
      FILES: true,
      FILE_UPLOAD: false
    },
    {
      role: 'guest',
      path: '/private',
      FILES: false // Guest cannot see private files
    },
    {
      role: 'user',
      path: '/public',
      FILES: true,
      FILE_UPLOAD: true
    },
    {
      role: 'user',
      path: '/private',
      FILES: true, // User can see private files
      FILE_UPLOAD: false
    },
    {
      role: 'admin',
      FILES: true,
      FILE_UPLOAD: true,
      FILE_REMOVE: true
    }
  ]
};

Next Steps