const express = require('express'); const router = express.Router(); const { body, validationResult } = require('express-validator'); const { authenticate, requireAdmin } = require('../middleware/auth'); const { requirePermission, requireAllPermissions, PERMISSIONS, DEFAULT_ROLES, clearAllPermissionCache, clearUserPermissionCache, logPermissionAction, getUserPermissions } = require('../middleware/rbac'); const { modifyLimiter, readLimiter } = require('../middleware/rateLimiter'); const { db } = require('../database/db'); const logger = require('../utils/logger'); const SecurityAuditLogger = require('../utils/securityAudit'); /** * Get all available permissions * Returns the complete permission catalog */ router.get('/permissions', authenticate, requirePermission('users.manage_roles'), readLimiter, (req, res) => { res.json({ permissions: Object.entries(PERMISSIONS).map(([key, description]) => ({ key, description })), categories: { 'User Management': Object.keys(PERMISSIONS).filter(k => k.startsWith('users.')), 'Session Management': Object.keys(PERMISSIONS).filter(k => k.startsWith('sessions.')), 'Content Management': Object.keys(PERMISSIONS).filter(k => k.startsWith('playlists.') || k.startsWith('channels.') || k.startsWith('favorites.') || k.startsWith('history.')), 'System & Settings': Object.keys(PERMISSIONS).filter(k => k.startsWith('settings.') || k.startsWith('stats.') || k.startsWith('backup.')), 'Security Management': Object.keys(PERMISSIONS).filter(k => k.startsWith('security.')), 'Search & Discovery': Object.keys(PERMISSIONS).filter(k => k.startsWith('search.')), 'VPN & Network': Object.keys(PERMISSIONS).filter(k => k.startsWith('vpn.')) } }); }); /** * Get all roles */ router.get('/roles', authenticate, requirePermission('users.view'), readLimiter, (req, res) => { db.all( `SELECT id, role_key, name, description, permissions, is_system_role, created_at, updated_at FROM roles ORDER BY is_system_role DESC, name ASC`, [], (err, roles) => { if (err) { logger.error('Error fetching roles:', err); return res.status(500).json({ error: 'Failed to fetch roles' }); } // Parse permissions JSON const rolesWithParsedPermissions = roles.map(role => ({ ...role, permissions: JSON.parse(role.permissions || '[]'), is_system_role: Boolean(role.is_system_role) })); res.json(rolesWithParsedPermissions); } ); }); /** * Get single role by key */ router.get('/roles/:roleKey', authenticate, requirePermission('users.view'), readLimiter, (req, res) => { const { roleKey } = req.params; db.get( `SELECT id, role_key, name, description, permissions, is_system_role, created_at, updated_at FROM roles WHERE role_key = ?`, [roleKey], (err, role) => { if (err) { logger.error('Error fetching role:', err); return res.status(500).json({ error: 'Failed to fetch role' }); } if (!role) { return res.status(404).json({ error: 'Role not found' }); } res.json({ ...role, permissions: JSON.parse(role.permissions || '[]'), is_system_role: Boolean(role.is_system_role) }); } ); }); /** * Create custom role * Only admins with users.manage_roles permission */ router.post('/roles', authenticate, requireAllPermissions(['users.manage_roles', 'users.create']), modifyLimiter, [ body('role_key').trim().isLength({ min: 2, max: 50 }).matches(/^[a-z_]+$/).withMessage('Role key must be lowercase with underscores only'), body('name').trim().isLength({ min: 2, max: 100 }), body('description').optional().trim().isLength({ max: 500 }), body('permissions').isArray().withMessage('Permissions must be an array'), body('permissions.*').isString().isIn(Object.keys(PERMISSIONS)).withMessage('Invalid permission') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { role_key, name, description, permissions } = req.body; try { // Check if role key already exists db.get('SELECT id FROM roles WHERE role_key = ?', [role_key], (err, existing) => { if (err) { logger.error('Error checking role existence:', err); return res.status(500).json({ error: 'Failed to create role' }); } if (existing) { return res.status(409).json({ error: 'Role key already exists' }); } // Create new role db.run( `INSERT INTO roles (role_key, name, description, permissions, is_system_role) VALUES (?, ?, ?, ?, 0)`, [role_key, name, description || '', JSON.stringify(permissions)], function(err) { if (err) { logger.error('Error creating role:', err); return res.status(500).json({ error: 'Failed to create role' }); } // Log action logPermissionAction( req.user.userId, 'role_created', 'role', this.lastID, null, { role_key, name, permissions }, req ); logger.info(`Role created: ${role_key} by user ${req.user.userId}`); // Fetch and return the created role db.get( 'SELECT id, role_key, name, description, permissions, is_system_role, created_at FROM roles WHERE id = ?', [this.lastID], (err, role) => { if (err) { return res.status(500).json({ error: 'Role created but failed to fetch details' }); } res.status(201).json({ ...role, permissions: JSON.parse(role.permissions), is_system_role: Boolean(role.is_system_role) }); } ); } ); }); } catch (error) { logger.error('Role creation error:', error); res.status(500).json({ error: 'Failed to create role' }); } } ); /** * Update role permissions * Cannot modify system roles */ router.patch('/roles/:roleKey', authenticate, requirePermission('users.manage_roles'), modifyLimiter, [ body('name').optional().trim().isLength({ min: 2, max: 100 }), body('description').optional().trim().isLength({ max: 500 }), body('permissions').optional().isArray(), body('permissions.*').optional().isString().isIn(Object.keys(PERMISSIONS)) ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { roleKey } = req.params; const { name, description, permissions } = req.body; try { // Check if role exists and is not a system role db.get('SELECT * FROM roles WHERE role_key = ?', [roleKey], (err, role) => { if (err) { logger.error('Error fetching role:', err); return res.status(500).json({ error: 'Failed to update role' }); } if (!role) { return res.status(404).json({ error: 'Role not found' }); } if (role.is_system_role) { return res.status(403).json({ error: 'Cannot modify system roles' }); } // Build update query const updates = []; const params = []; if (name !== undefined) { updates.push('name = ?'); params.push(name); } if (description !== undefined) { updates.push('description = ?'); params.push(description); } if (permissions !== undefined) { updates.push('permissions = ?'); params.push(JSON.stringify(permissions)); } if (updates.length === 0) { return res.status(400).json({ error: 'No fields to update' }); } updates.push('updated_at = CURRENT_TIMESTAMP'); params.push(roleKey); // Update role db.run( `UPDATE roles SET ${updates.join(', ')} WHERE role_key = ?`, params, function(err) { if (err) { logger.error('Error updating role:', err); return res.status(500).json({ error: 'Failed to update role' }); } if (this.changes === 0) { return res.status(404).json({ error: 'Role not found' }); } // Log action logPermissionAction( req.user.userId, 'role_updated', 'role', role.id, { name: role.name, description: role.description, permissions: JSON.parse(role.permissions) }, { name, description, permissions }, req ); // Clear permission cache as role permissions changed clearAllPermissionCache(); logger.info(`Role updated: ${roleKey} by user ${req.user.userId}`); // Fetch and return updated role db.get( 'SELECT id, role_key, name, description, permissions, is_system_role, updated_at FROM roles WHERE role_key = ?', [roleKey], (err, updatedRole) => { if (err) { return res.status(500).json({ error: 'Role updated but failed to fetch details' }); } res.json({ ...updatedRole, permissions: JSON.parse(updatedRole.permissions), is_system_role: Boolean(updatedRole.is_system_role) }); } ); } ); }); } catch (error) { logger.error('Role update error:', error); res.status(500).json({ error: 'Failed to update role' }); } } ); /** * Delete custom role * Cannot delete system roles or roles assigned to users */ router.delete('/roles/:roleKey', authenticate, requirePermission('users.manage_roles'), modifyLimiter, async (req, res) => { const { roleKey } = req.params; try { // Check if role exists db.get('SELECT * FROM roles WHERE role_key = ?', [roleKey], (err, role) => { if (err) { logger.error('Error fetching role:', err); return res.status(500).json({ error: 'Failed to delete role' }); } if (!role) { return res.status(404).json({ error: 'Role not found' }); } if (role.is_system_role) { return res.status(403).json({ error: 'Cannot delete system roles' }); } // Check if role is assigned to any users db.get('SELECT COUNT(*) as count FROM users WHERE role = ?', [roleKey], (err, result) => { if (err) { logger.error('Error checking role usage:', err); return res.status(500).json({ error: 'Failed to delete role' }); } if (result.count > 0) { return res.status(409).json({ error: 'Cannot delete role that is assigned to users', users_count: result.count }); } // Delete role db.run('DELETE FROM roles WHERE role_key = ?', [roleKey], function(err) { if (err) { logger.error('Error deleting role:', err); return res.status(500).json({ error: 'Failed to delete role' }); } // Log action logPermissionAction( req.user.userId, 'role_deleted', 'role', role.id, { role_key: roleKey, name: role.name }, null, req ); logger.info(`Role deleted: ${roleKey} by user ${req.user.userId}`); res.json({ message: 'Role deleted successfully' }); }); }); }); } catch (error) { logger.error('Role deletion error:', error); res.status(500).json({ error: 'Failed to delete role' }); } } ); /** * Get user's current permissions */ router.get('/my-permissions', authenticate, readLimiter, async (req, res) => { try { const permissions = await getUserPermissions(req.user.userId); // Get role info db.get( 'SELECT u.role, r.name as role_name, r.description as role_description FROM users u LEFT JOIN roles r ON u.role = r.role_key WHERE u.id = ?', [req.user.userId], (err, roleInfo) => { if (err) { logger.error('Error fetching role info:', err); return res.status(500).json({ error: 'Failed to fetch permissions' }); } res.json({ role: roleInfo?.role || 'unknown', role_name: roleInfo?.role_name || 'Unknown', role_description: roleInfo?.role_description || '', permissions, permission_details: permissions.map(p => ({ key: p, description: PERMISSIONS[p] || 'Unknown permission' })) }); } ); } catch (error) { logger.error('Error fetching user permissions:', error); res.status(500).json({ error: 'Failed to fetch permissions' }); } }); /** * Assign role to user * Requires users.manage_roles permission */ router.post('/users/:userId/role', authenticate, requirePermission('users.manage_roles'), modifyLimiter, [ body('role').trim().notEmpty().withMessage('Role is required') ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { userId } = req.params; const { role } = req.body; try { // Check if role exists db.get('SELECT role_key FROM roles WHERE role_key = ?', [role], (err, roleExists) => { if (err) { logger.error('Error checking role:', err); return res.status(500).json({ error: 'Failed to assign role' }); } if (!roleExists) { return res.status(404).json({ error: 'Role not found' }); } // Check if user exists db.get('SELECT id, username, role FROM users WHERE id = ?', [userId], (err, user) => { if (err) { logger.error('Error fetching user:', err); return res.status(500).json({ error: 'Failed to assign role' }); } if (!user) { return res.status(404).json({ error: 'User not found' }); } // Prevent modifying own role if (parseInt(userId) === req.user.userId) { return res.status(403).json({ error: 'Cannot modify your own role' }); } const oldRole = user.role; // Update user role db.run( 'UPDATE users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [role, userId], async function(err) { if (err) { logger.error('Error updating user role:', err); return res.status(500).json({ error: 'Failed to assign role' }); } // Clear user's permission cache clearUserPermissionCache(parseInt(userId)); const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress; const userAgent = req.headers['user-agent']; // CWE-778: Log comprehensive privilege change await SecurityAuditLogger.logPrivilegeChange(parseInt(userId), 'role_change', { ip, userAgent, previousRole: oldRole, newRole: role, changedBy: req.user.userId, changedByUsername: req.user.username || 'system', targetUsername: user.username }); // Log action logPermissionAction( req.user.userId, 'role_assigned', 'user', parseInt(userId), { role: oldRole }, { role }, req ); logger.info(`Role assigned: ${role} to user ${userId} by ${req.user.userId}`); res.json({ message: 'Role assigned successfully', user_id: userId, old_role: oldRole, new_role: role }); } ); }); }); } catch (error) { logger.error('Role assignment error:', error); res.status(500).json({ error: 'Failed to assign role' }); } } ); /** * Get permission audit log * Admin only */ router.get('/audit-log', authenticate, requirePermission('security.view_audit'), readLimiter, async (req, res) => { const { limit = 100, offset = 0, userId, action, targetType } = req.query; try { let query = ` SELECT pal.*, u.username FROM permission_audit_log pal JOIN users u ON pal.user_id = u.id WHERE 1=1 `; const params = []; if (userId) { query += ' AND pal.user_id = ?'; params.push(userId); } if (action) { query += ' AND pal.action = ?'; params.push(action); } if (targetType) { query += ' AND pal.target_type = ?'; params.push(targetType); } query += ' ORDER BY pal.created_at DESC LIMIT ? OFFSET ?'; params.push(parseInt(limit), parseInt(offset)); db.all(query, params, (err, logs) => { if (err) { logger.error('Error fetching audit log:', err); return res.status(500).json({ error: 'Failed to fetch audit log' }); } // Parse JSON fields const parsedLogs = logs.map(log => ({ ...log, old_value: log.old_value ? JSON.parse(log.old_value) : null, new_value: log.new_value ? JSON.parse(log.new_value) : null })); res.json({ logs: parsedLogs, limit: parseInt(limit), offset: parseInt(offset) }); }); } catch (error) { logger.error('Audit log fetch error:', error); res.status(500).json({ error: 'Failed to fetch audit log' }); } } ); /** * Get permission statistics * Shows which permissions are most used */ router.get('/stats', authenticate, requirePermission('security.view_audit'), readLimiter, async (req, res) => { try { // Get role distribution db.all( `SELECT r.name, r.role_key, COUNT(u.id) as user_count FROM roles r LEFT JOIN users u ON r.role_key = u.role GROUP BY r.role_key ORDER BY user_count DESC`, [], (err, roleStats) => { if (err) { logger.error('Error fetching role stats:', err); return res.status(500).json({ error: 'Failed to fetch statistics' }); } // Get recent permission actions db.all( `SELECT action, COUNT(*) as count FROM permission_audit_log WHERE created_at >= datetime('now', '-30 days') GROUP BY action ORDER BY count DESC`, [], (err, actionStats) => { if (err) { logger.error('Error fetching action stats:', err); return res.status(500).json({ error: 'Failed to fetch statistics' }); } res.json({ role_distribution: roleStats, recent_actions: actionStats, total_permissions: Object.keys(PERMISSIONS).length, total_roles: roleStats.length }); } ); } ); } catch (error) { logger.error('Stats fetch error:', error); res.status(500).json({ error: 'Failed to fetch statistics' }); } } ); module.exports = router;