streamflow/backend/routes/rbac.js

621 lines
20 KiB
JavaScript
Raw Normal View History

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;