streamflow/backend/middleware/rbac.js
2025-12-17 00:42:43 +00:00

525 lines
15 KiB
JavaScript

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
};