- 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
175 lines
7.6 KiB
JavaScript
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';
|
|
}
|
|
};
|