const db = require('./db'); // ─── Permission Resolution ─── function hasPermission(userId, app, feature, action) { // Get all roles for this user where scope matches const roles = db.prepare(` SELECT r.id, r.name FROM user_roles ur JOIN roles r ON r.id = ur.role_id WHERE ur.user_id = ? AND (ur.scope = '*' OR ur.scope = ?) `).all(userId, app); if (roles.length === 0) return { allowed: false }; for (const role of roles) { // Check permissions for this role const perms = db.prepare(` SELECT p.app, p.feature, p.action FROM role_permissions rp JOIN permissions p ON p.id = rp.permission_id WHERE rp.role_id = ? `).all(role.id); for (const p of perms) { if ((p.app === '*' || p.app === app) && (p.feature === '*' || p.feature === feature) && (p.action === '*' || p.action === action)) { return { allowed: true, role: role.name, matched: `${p.app}.${p.feature}.${p.action}` }; } } } return { allowed: false }; } function hasPermissionByEmail(email, app, feature, action) { const user = db.prepare('SELECT id FROM users WHERE email = ? COLLATE NOCASE').get(email); if (!user) return { allowed: false, reason: 'user_not_found' }; return hasPermission(user.id, app, feature, action); } function getEffectivePermissions(userId, app) { const scopeFilter = app ? 'AND (ur.scope = ? OR ur.scope = ?)' : 'AND 1=1'; const params = app ? [userId, '*', app] : [userId]; const roles = db.prepare(` SELECT r.id, r.name, ur.scope FROM user_roles ur JOIN roles r ON r.id = ur.role_id WHERE ur.user_id = ? ${app ? 'AND (ur.scope = ? OR ur.scope = ?)' : ''} `).all(...params); const permissions = new Set(); const roleNames = []; for (const role of roles) { roleNames.push({ role: role.name, scope: role.scope }); const perms = db.prepare(` SELECT p.app, p.feature, p.action FROM role_permissions rp JOIN permissions p ON p.id = rp.permission_id WHERE rp.role_id = ? `).all(role.id); for (const p of perms) { permissions.add(`${p.app}.${p.feature}.${p.action}`); } } return { roles: roleNames, permissions: [...permissions] }; } function getEffectivePermissionsByEmail(email, app) { const user = db.prepare('SELECT id FROM users WHERE email = ? COLLATE NOCASE').get(email); if (!user) return { roles: [], permissions: [] }; return getEffectivePermissions(user.id, app); } // ─── Role CRUD ─── function listRoles() { return db.prepare(` SELECT r.*, (SELECT COUNT(*) FROM role_permissions WHERE role_id = r.id) as perm_count, (SELECT COUNT(*) FROM user_roles WHERE role_id = r.id) as user_count FROM roles r ORDER BY r.priority ASC `).all(); } function getRole(name) { const role = db.prepare('SELECT * FROM roles WHERE name = ?').get(name); if (!role) return null; role.permissions = db.prepare(` SELECT p.* FROM role_permissions rp JOIN permissions p ON p.id = rp.permission_id WHERE rp.role_id = ? ORDER BY p.app, p.feature, p.action `).all(role.id); role.users = db.prepare(` SELECT u.email, u.name, ur.scope, ur.granted_at FROM user_roles ur JOIN users u ON u.id = ur.user_id WHERE ur.role_id = ? `).all(role.id); return role; } function createRole(name, displayName, description, isSystem = false, priority = 100) { return db.prepare(` INSERT INTO roles (name, display_name, description, is_system, priority) VALUES (?, ?, ?, ?, ?) `).run(name, displayName, description, isSystem ? 1 : 0, priority); } function updateRole(name, updates) { const role = db.prepare('SELECT * FROM roles WHERE name = ?').get(name); if (!role) return null; const { display_name, description, priority } = updates; db.prepare(` UPDATE roles SET display_name = COALESCE(?, display_name), description = COALESCE(?, description), priority = COALESCE(?, priority), updated_at = datetime('now') WHERE name = ? `).run(display_name || null, description || null, priority || null, name); return db.prepare('SELECT * FROM roles WHERE name = ?').get(name); } function deleteRole(name) { const role = db.prepare('SELECT * FROM roles WHERE name = ?').get(name); if (!role) return { error: 'not_found' }; if (role.is_system) return { error: 'system_role' }; db.prepare('DELETE FROM roles WHERE id = ?').run(role.id); return { deleted: true }; } function setRolePermissions(roleName, permissionIds) { const role = db.prepare('SELECT id FROM roles WHERE name = ?').get(roleName); if (!role) return { error: 'role_not_found' }; const tx = db.transaction(() => { db.prepare('DELETE FROM role_permissions WHERE role_id = ?').run(role.id); const insert = db.prepare('INSERT INTO role_permissions (role_id, permission_id) VALUES (?, ?)'); for (const pid of permissionIds) { insert.run(role.id, pid); } }); tx(); return { ok: true, count: permissionIds.length }; } function addRolePermissionsByPattern(roleName, app, feature, action) { const role = db.prepare('SELECT id FROM roles WHERE name = ?').get(roleName); if (!role) return { error: 'role_not_found' }; // Find or create the permission let perm = db.prepare('SELECT id FROM permissions WHERE app = ? AND feature = ? AND action = ?').get(app, feature, action); if (!perm) { const r = db.prepare('INSERT INTO permissions (app, feature, action) VALUES (?, ?, ?)').run(app, feature, action); perm = { id: r.lastInsertRowid }; } try { db.prepare('INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES (?, ?)').run(role.id, perm.id); } catch (e) { /* already exists */ } return { ok: true }; } // ─── User-Role Assignment ─── function getUserRoles(email) { const user = db.prepare('SELECT id FROM users WHERE email = ? COLLATE NOCASE').get(email); if (!user) return []; return db.prepare(` SELECT r.name, r.display_name, ur.scope, ur.granted_at, ur.granted_by FROM user_roles ur JOIN roles r ON r.id = ur.role_id WHERE ur.user_id = ? ORDER BY r.priority `).all(user.id); } function assignRole(email, roleName, scope, grantedBy) { const user = db.prepare('SELECT id FROM users WHERE email = ? COLLATE NOCASE').get(email); if (!user) return { error: 'user_not_found' }; const role = db.prepare('SELECT id FROM roles WHERE name = ?').get(roleName); if (!role) return { error: 'role_not_found' }; db.prepare(` INSERT OR REPLACE INTO user_roles (user_id, role_id, scope, granted_by) VALUES (?, ?, ?, ?) `).run(user.id, role.id, scope || '*', grantedBy || 'system'); return { ok: true }; } function removeRole(email, roleName, scope) { const user = db.prepare('SELECT id FROM users WHERE email = ? COLLATE NOCASE').get(email); if (!user) return { error: 'user_not_found' }; const role = db.prepare('SELECT id FROM roles WHERE name = ?').get(roleName); if (!role) return { error: 'role_not_found' }; db.prepare('DELETE FROM user_roles WHERE user_id = ? AND role_id = ? AND scope = ?') .run(user.id, role.id, scope || '*'); return { ok: true }; } // ─── Feature Registration ─── function registerFeatures(appId, features) { const insert = db.prepare(` INSERT OR IGNORE INTO permissions (app, feature, action, display_name, category) VALUES (?, ?, ?, ?, ?) `); let count = 0; const tx = db.transaction(() => { for (const f of features) { for (const action of (f.actions || ['read'])) { insert.run(appId, f.feature, action, f.display_name || f.feature, f.category || 'general'); count++; } } // Mark service as features_registered db.prepare('UPDATE services SET features_registered = 1, updated_at = datetime(\'now\') WHERE id = ?').run(appId); }); tx(); return { ok: true, registered: count }; } function listFeatures(appId) { const where = appId ? 'WHERE app = ?' : ''; const params = appId ? [appId] : []; return db.prepare(`SELECT * FROM permissions ${where} ORDER BY app, category, feature, action`).all(...params); } // ─── Audit ─── function audit(actorEmail, action, targetType, targetId, detail, ip, service) { db.prepare(` INSERT INTO audit_log (actor_email, action, target_type, target_id, detail, ip, service) VALUES (?, ?, ?, ?, ?, ?, ?) `).run(actorEmail, action, targetType || null, targetId || null, detail ? JSON.stringify(detail) : null, ip || null, service || null); } function queryAudit({ actor, action, targetType, limit, offset, since, until } = {}) { let where = []; let params = []; if (actor) { where.push('actor_email = ?'); params.push(actor); } if (action) { where.push('action LIKE ?'); params.push(action + '%'); } if (targetType) { where.push('target_type = ?'); params.push(targetType); } if (since) { where.push('timestamp >= ?'); params.push(since); } if (until) { where.push('timestamp <= ?'); params.push(until); } const whereClause = where.length ? 'WHERE ' + where.join(' AND ') : ''; params.push(limit || 100, offset || 0); return db.prepare(` SELECT * FROM audit_log ${whereClause} ORDER BY timestamp DESC LIMIT ? OFFSET ? `).all(...params); } module.exports = { hasPermission, hasPermissionByEmail, getEffectivePermissions, getEffectivePermissionsByEmail, listRoles, getRole, createRole, updateRole, deleteRole, setRolePermissions, addRolePermissionsByPattern, getUserRoles, assignRole, removeRole, registerFeatures, listFeatures, audit, queryAudit, };