const logger = require('../utils/logger'); const { db } = require('../database/db'); /** * RBAC (Role-Based Access Control) Middleware * * Implements granular permission checking following the principle of least privilege. * Each user has roles, and each role has specific permissions. * This prevents over-privileged accounts and limits attack surface. */ // Define all available permissions in the system const PERMISSIONS = { // User Management 'users.view': 'View user list and details', 'users.create': 'Create new users', 'users.edit': 'Edit existing users', 'users.delete': 'Delete users', 'users.manage_roles': 'Assign and modify user roles', 'users.unlock': 'Unlock locked user accounts', 'users.reset_password': 'Reset user passwords', // Session Management 'sessions.view_own': 'View own active sessions', 'sessions.view_all': 'View all user sessions', 'sessions.terminate_own': 'Terminate own sessions', 'sessions.terminate_any': 'Terminate any user session', 'sessions.view_stats': 'View session statistics', // Content Management 'playlists.view': 'View playlists', 'playlists.create': 'Create playlists', 'playlists.edit': 'Edit playlists', 'playlists.delete': 'Delete playlists', 'playlists.import': 'Import M3U files', 'channels.view': 'View channels', 'channels.edit': 'Edit channel details', 'channels.upload_logo': 'Upload custom channel logos', 'channels.delete_logo': 'Delete custom channel logos', 'favorites.view': 'View favorites', 'favorites.manage': 'Add/remove favorites', 'history.view_own': 'View own watch history', 'history.view_all': 'View all user watch history', 'history.delete_own': 'Delete own watch history', 'history.delete_any': 'Delete any user watch history', // System & Settings 'settings.view': 'View application settings', 'settings.edit': 'Modify application settings', 'stats.view': 'View analytics and statistics', 'stats.view_detailed': 'View detailed analytics (user activity, etc.)', 'backup.view': 'View available backups', 'backup.create': 'Create backups', 'backup.restore': 'Restore from backups', 'backup.delete': 'Delete backups', 'backup.download': 'Download backups', // Security Management 'security.view_sessions': 'View security session dashboard', 'security.view_csp': 'View CSP violation dashboard', 'security.manage_2fa': 'Manage two-factor authentication', 'security.view_audit': 'View audit logs', // Search & Discovery 'search.use': 'Use search functionality', 'search.admin': 'Search users and system settings', // VPN & Network 'vpn.view': 'View VPN settings', 'vpn.configure': 'Configure VPN settings', 'vpn.connect': 'Connect/disconnect VPN' }; // Default role definitions with their permissions const DEFAULT_ROLES = { 'admin': { name: 'Administrator', description: 'Full system access', permissions: Object.keys(PERMISSIONS), // Admins have all permissions is_system_role: true }, 'moderator': { name: 'Moderator', description: 'Content management and user support', permissions: [ // User viewing (but not management) 'users.view', // Content management 'playlists.view', 'playlists.create', 'playlists.edit', 'playlists.delete', 'playlists.import', 'channels.view', 'channels.edit', 'channels.upload_logo', 'channels.delete_logo', // History management 'history.view_all', 'history.delete_any', // Settings (view only) 'settings.view', // Statistics 'stats.view', 'stats.view_detailed', // Own sessions 'sessions.view_own', 'sessions.terminate_own', // Own favorites 'favorites.view', 'favorites.manage', // Search 'search.use', // Own security 'security.manage_2fa' ], is_system_role: true }, 'user': { name: 'Regular User', description: 'Standard user with content access', permissions: [ // Own playlists 'playlists.view', 'playlists.create', 'playlists.edit', 'playlists.delete', 'playlists.import', // Channels (view and customize) 'channels.view', 'channels.upload_logo', 'channels.delete_logo', // Own favorites 'favorites.view', 'favorites.manage', // Own history 'history.view_own', 'history.delete_own', // Own settings 'settings.view', 'settings.edit', // Own sessions 'sessions.view_own', 'sessions.terminate_own', // Search (basic) 'search.use', // Own security 'security.manage_2fa', // VPN (if enabled) 'vpn.view', 'vpn.configure', 'vpn.connect' ], is_system_role: true }, 'viewer': { name: 'Viewer', description: 'Read-only access for content viewing', permissions: [ // View only 'playlists.view', 'channels.view', 'favorites.view', 'favorites.manage', 'history.view_own', 'history.delete_own', 'settings.view', 'sessions.view_own', 'sessions.terminate_own', 'search.use', 'security.manage_2fa' ], is_system_role: true } }; /** * Cache for user permissions to reduce database queries * Format: { userId: { permissions: [...], expires: timestamp } } */ const permissionCache = new Map(); const CACHE_TTL = 5 * 60 * 1000; // 5 minutes /** * Clear permission cache for a specific user */ const clearUserPermissionCache = (userId) => { permissionCache.delete(userId); logger.info(`Permission cache cleared for user ${userId}`); }; /** * Clear entire permission cache (call after role/permission changes) */ const clearAllPermissionCache = () => { permissionCache.clear(); logger.info('All permission cache cleared'); }; /** * Get user permissions from database with caching */ const getUserPermissions = (userId) => { return new Promise((resolve, reject) => { // Check cache first const cached = permissionCache.get(userId); if (cached && cached.expires > Date.now()) { return resolve(cached.permissions); } // Query database db.get( `SELECT u.role, r.permissions FROM users u LEFT JOIN roles r ON u.role = r.role_key WHERE u.id = ? AND u.is_active = 1`, [userId], (err, result) => { if (err) { logger.error('Error fetching user permissions:', err); return reject(err); } if (!result) { return reject(new Error('User not found or inactive')); } // Parse permissions (stored as JSON string) let permissions = []; try { if (result.permissions) { permissions = JSON.parse(result.permissions); } else { // Fallback to default role permissions if not in database const defaultRole = DEFAULT_ROLES[result.role]; permissions = defaultRole ? defaultRole.permissions : []; } } catch (parseErr) { logger.error('Error parsing permissions:', parseErr); permissions = []; } // Cache the result permissionCache.set(userId, { permissions, expires: Date.now() + CACHE_TTL }); resolve(permissions); } ); }); }; /** * Check if user has a specific permission */ const hasPermission = async (userId, permission) => { try { const permissions = await getUserPermissions(userId); return permissions.includes(permission); } catch (error) { logger.error('Permission check failed:', error); return false; } }; /** * Check if user has ANY of the specified permissions */ const hasAnyPermission = async (userId, permissionList) => { try { const permissions = await getUserPermissions(userId); return permissionList.some(p => permissions.includes(p)); } catch (error) { logger.error('Permission check failed:', error); return false; } }; /** * Check if user has ALL of the specified permissions */ const hasAllPermissions = async (userId, permissionList) => { try { const permissions = await getUserPermissions(userId); return permissionList.every(p => permissions.includes(p)); } catch (error) { logger.error('Permission check failed:', error); return false; } }; /** * Middleware: Require specific permission(s) * Usage: requirePermission('users.view') * Usage: requirePermission(['users.view', 'users.edit']) - requires ANY */ const requirePermission = (requiredPermissions) => { // Normalize to array const permissionList = Array.isArray(requiredPermissions) ? requiredPermissions : [requiredPermissions]; return async (req, res, next) => { if (!req.user || !req.user.userId) { return res.status(401).json({ error: 'Authentication required', code: 'AUTH_REQUIRED' }); } try { // Bypass permission check for user ID 1 (first admin) or users with role 'admin' if (req.user.userId === 1 || req.user.role === 'admin') { req.userPermissions = Object.keys(PERMISSIONS); // Grant all permissions return next(); } const userPermissions = await getUserPermissions(req.user.userId); // Check if user has any of the required permissions const hasAccess = permissionList.some(p => userPermissions.includes(p)); if (!hasAccess) { logger.warn(`Access denied: User ${req.user.userId} lacks permission(s): ${permissionList.join(', ')}`); return res.status(403).json({ error: 'Insufficient permissions', code: 'INSUFFICIENT_PERMISSIONS', required: permissionList, details: 'You do not have the required permissions to perform this action' }); } // Attach user permissions to request for further checks req.userPermissions = userPermissions; next(); } catch (error) { logger.error('Permission check error:', error); res.status(500).json({ error: 'Permission validation failed', code: 'PERMISSION_CHECK_FAILED' }); } }; }; /** * Middleware: Require ALL specified permissions * Usage: requireAllPermissions(['users.view', 'users.edit']) */ const requireAllPermissions = (permissionList) => { return async (req, res, next) => { if (!req.user || !req.user.userId) { return res.status(401).json({ error: 'Authentication required', code: 'AUTH_REQUIRED' }); } try { // Bypass permission check for user ID 1 (first admin) or users with role 'admin' if (req.user.userId === 1 || req.user.role === 'admin') { req.userPermissions = Object.keys(PERMISSIONS); // Grant all permissions return next(); } const userPermissions = await getUserPermissions(req.user.userId); // Check if user has ALL required permissions const hasAccess = permissionList.every(p => userPermissions.includes(p)); if (!hasAccess) { logger.warn(`Access denied: User ${req.user.userId} lacks all permissions: ${permissionList.join(', ')}`); return res.status(403).json({ error: 'Insufficient permissions', code: 'INSUFFICIENT_PERMISSIONS', required: permissionList, details: 'You do not have all the required permissions to perform this action' }); } req.userPermissions = userPermissions; next(); } catch (error) { logger.error('Permission check error:', error); res.status(500).json({ error: 'Permission validation failed', code: 'PERMISSION_CHECK_FAILED' }); } }; }; /** * Initialize roles table and seed default roles */ const initializeRoles = () => { db.serialize(() => { // Create roles table db.run(`CREATE TABLE IF NOT EXISTS roles ( id INTEGER PRIMARY KEY AUTOINCREMENT, role_key TEXT UNIQUE NOT NULL, name TEXT NOT NULL, description TEXT, permissions TEXT NOT NULL, is_system_role BOOLEAN DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )`, (err) => { if (err) { logger.error('Failed to create roles table:', err); return; } // Seed default roles Object.entries(DEFAULT_ROLES).forEach(([roleKey, roleData]) => { db.run( `INSERT OR IGNORE INTO roles (role_key, name, description, permissions, is_system_role) VALUES (?, ?, ?, ?, ?)`, [ roleKey, roleData.name, roleData.description, JSON.stringify(roleData.permissions), roleData.is_system_role ? 1 : 0 ], (err) => { if (err) { logger.error(`Failed to seed role ${roleKey}:`, err); } else { logger.info(`✓ Role seeded: ${roleKey}`); } } ); }); // Create permission audit log table db.run(`CREATE TABLE IF NOT EXISTS permission_audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, action TEXT NOT NULL, target_type TEXT NOT NULL, target_id INTEGER, old_value TEXT, new_value TEXT, ip_address TEXT, user_agent TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`, (err) => { if (err) { logger.error('Failed to create permission_audit_log table:', err); } else { logger.info('✓ Permission audit log table created'); } }); }); }); }; /** * Log permission-related actions for audit trail */ const logPermissionAction = (userId, action, targetType, targetId, oldValue, newValue, req) => { const ipAddress = req?.ip || req?.connection?.remoteAddress || 'unknown'; const userAgent = req?.headers['user-agent'] || 'unknown'; db.run( `INSERT INTO permission_audit_log (user_id, action, target_type, target_id, old_value, new_value, ip_address, user_agent) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [ userId, action, targetType, targetId || null, oldValue ? JSON.stringify(oldValue) : null, newValue ? JSON.stringify(newValue) : null, ipAddress, userAgent ], (err) => { if (err) { logger.error('Failed to log permission action:', err); } } ); }; module.exports = { PERMISSIONS, DEFAULT_ROLES, requirePermission, requireAllPermissions, hasPermission, hasAnyPermission, hasAllPermissions, getUserPermissions, clearUserPermissionCache, clearAllPermissionCache, initializeRoles, logPermissionAction };