access-manager/rbac-routes.js
TARS 0b0871ffea feat: Access Manager v3 — RBAC engine, SQLite, permission system
- SQLite database with full schema: users, roles, permissions,
  role_permissions, user_roles, services, audit_log
- RBAC engine with wildcard permission resolution (*.*.*)
- Automatic v2→v3 migration from JSON files
- 5 default roles: super_admin, admin, editor, user, viewer
- Feature registration for APP, GGL, FDX (119 permissions)
- 8 services seeded
- Full API: roles CRUD, permission check, user-role assignment,
  feature registration, audit log, stats
- Backward compatible with existing auth flows
2026-04-16 00:57:27 +00:00

175 lines
7.6 KiB
JavaScript

// ═══════════════════════════════════════
// 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';
}
};