// ═══════════════════════════════════════ // RBAC API ROUTES — bolt onto existing server // ═══════════════════════════════════════ const db = require('./lib/db'); const rbac = require('./lib/rbac'); module.exports = function mountRbacRoutes(app, requireAdmin) { // ─── Permission Check (for apps — service-to-service) ─── app.post('/api/permissions/check', (req, res) => { const { email, app: appId, feature, action } = req.body; if (!email || !appId || !feature || !action) { return res.status(400).json({ error: 'email, app, feature, action required' }); } const result = rbac.hasPermissionByEmail(email, appId, feature, action); res.json(result); }); app.get('/api/permissions/user/:email', requireAdmin, (req, res) => { const result = rbac.getEffectivePermissionsByEmail(req.params.email); res.json({ email: req.params.email, ...result }); }); app.get('/api/permissions/user/:email/:app', requireAdmin, (req, res) => { const result = rbac.getEffectivePermissionsByEmail(req.params.email, req.params.app); res.json({ email: req.params.email, app: req.params.app, ...result }); }); // ─── Roles CRUD ─── app.get('/api/roles', requireAdmin, (req, res) => { res.json(rbac.listRoles()); }); app.post('/api/roles', requireAdmin, (req, res) => { const { name, display_name, description, priority } = req.body; if (!name || !display_name) return res.status(400).json({ error: 'name and display_name required' }); try { rbac.createRole(name, display_name, description, false, priority || 100); rbac.audit(getActorEmail(req), 'role.create', 'role', name, { display_name }); res.status(201).json(rbac.getRole(name)); } catch (e) { res.status(409).json({ error: e.message }); } }); app.get('/api/roles/:name', requireAdmin, (req, res) => { const role = rbac.getRole(req.params.name); if (!role) return res.status(404).json({ error: 'Role not found' }); res.json(role); }); app.put('/api/roles/:name', requireAdmin, (req, res) => { const result = rbac.updateRole(req.params.name, req.body); if (!result) return res.status(404).json({ error: 'Role not found' }); rbac.audit(getActorEmail(req), 'role.update', 'role', req.params.name, req.body); res.json(result); }); app.delete('/api/roles/:name', requireAdmin, (req, res) => { const result = rbac.deleteRole(req.params.name); if (result.error === 'not_found') return res.status(404).json({ error: 'Role not found' }); if (result.error === 'system_role') return res.status(400).json({ error: 'Cannot delete system roles' }); rbac.audit(getActorEmail(req), 'role.delete', 'role', req.params.name); res.json(result); }); // ─── Role Permissions ─── app.put('/api/roles/:name/permissions', requireAdmin, (req, res) => { const { permission_ids } = req.body; if (!Array.isArray(permission_ids)) return res.status(400).json({ error: 'permission_ids array required' }); const result = rbac.setRolePermissions(req.params.name, permission_ids); if (result.error) return res.status(404).json({ error: result.error }); rbac.audit(getActorEmail(req), 'role.permissions.set', 'role', req.params.name, { count: permission_ids.length }); res.json(result); }); app.post('/api/roles/:name/permissions', requireAdmin, (req, res) => { const { app: appId, feature, action } = req.body; if (!appId || !feature || !action) return res.status(400).json({ error: 'app, feature, action required' }); const result = rbac.addRolePermissionsByPattern(req.params.name, appId, feature, action); if (result.error) return res.status(404).json({ error: result.error }); rbac.audit(getActorEmail(req), 'role.permissions.add', 'role', req.params.name, { app: appId, feature, action }); res.json(result); }); // ─── User-Role Assignment ─── app.get('/api/users/:email/roles', requireAdmin, (req, res) => { res.json(rbac.getUserRoles(req.params.email)); }); app.post('/api/users/:email/roles', requireAdmin, (req, res) => { const { role, scope } = req.body; if (!role) return res.status(400).json({ error: 'role required' }); const result = rbac.assignRole(req.params.email, role, scope || '*', getActorEmail(req)); if (result.error) return res.status(404).json({ error: result.error }); rbac.audit(getActorEmail(req), 'role.assign', 'user', req.params.email, { role, scope }); res.json(result); }); app.delete('/api/users/:email/roles/:role', requireAdmin, (req, res) => { const scope = req.query.scope || '*'; const result = rbac.removeRole(req.params.email, req.params.role, scope); if (result.error) return res.status(404).json({ error: result.error }); rbac.audit(getActorEmail(req), 'role.remove', 'user', req.params.email, { role: req.params.role, scope }); res.json(result); }); // ─── Feature Registration ─── app.post('/api/features/register', (req, res) => { const { app: appId, features } = req.body; if (!appId || !Array.isArray(features)) return res.status(400).json({ error: 'app and features array required' }); const result = rbac.registerFeatures(appId, features); res.json(result); }); app.get('/api/features', requireAdmin, (req, res) => { res.json(rbac.listFeatures()); }); app.get('/api/features/:app', requireAdmin, (req, res) => { res.json(rbac.listFeatures(req.params.app)); }); // ─── All Permissions (for admin UI matrix) ─── app.get('/api/permissions', requireAdmin, (req, res) => { const perms = db.prepare('SELECT * FROM permissions ORDER BY app, category, feature, action').all(); res.json(perms); }); // ─── Audit Log ─── app.get('/api/audit', requireAdmin, (req, res) => { const { actor, action, target_type, limit, offset, since, until } = req.query; res.json(rbac.queryAudit({ actor, action, targetType: target_type, limit: parseInt(limit) || 100, offset: parseInt(offset) || 0, since, until })); }); app.get('/api/audit/user/:email', requireAdmin, (req, res) => { res.json(rbac.queryAudit({ actor: req.params.email, limit: parseInt(req.query.limit) || 50 })); }); // ─── Services (from DB now) ─── app.get('/api/services/list', requireAdmin, (req, res) => { res.json(db.prepare('SELECT * FROM services ORDER BY display_name').all()); }); // ─── Stats (for dashboard) ─── app.get('/api/stats', requireAdmin, (req, res) => { res.json({ users: db.prepare('SELECT COUNT(*) as c FROM users').get().c, roles: db.prepare('SELECT COUNT(*) as c FROM roles').get().c, permissions: db.prepare('SELECT COUNT(*) as c FROM permissions').get().c, services: db.prepare('SELECT COUNT(*) as c FROM services').get().c, assignments: db.prepare('SELECT COUNT(*) as c FROM user_roles').get().c, recentAudit: db.prepare("SELECT COUNT(*) as c FROM audit_log WHERE timestamp > datetime('now', '-24 hours')").get().c, }); }); // Helper function getActorEmail(req) { if (req.session?.email) return req.session.email; const amSession = req.cookies?.['_am_session']; if (amSession) { try { const jwt = require('jsonwebtoken'); const decoded = jwt.verify(amSession, process.env.JWT_SECRET || require('./lib/db').__jwt_secret); return decoded.email; } catch {} } return req.headers['x-email'] || req.headers['x-forwarded-email'] || 'unknown'; } };