Access Control (ACL)
This document provides a comprehensive guide to implementing access control rules in Jodit Connector Node.js.
Table of Contents
- Overview
- Basic Rules
- Available Actions
- Rule Matching
- Dynamic Rules with Functions
- Dynamic Access Control
- Advanced Examples
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
- Always implement caching for database/API calls to avoid performance issues
- Set appropriate TTL (cache lifetime) based on how often rules change
- Provide fallback rules in case loading fails
- Monitor performance - async rule loading adds latency to every request
- Use sync functions when rules depend only on application state
- Log rule changes for audit and debugging
- 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
- Authentication - Learn about authentication methods
- Configuration - Explore all configuration options
- API Usage - See complete usage examples