access-manager/lib/rbac.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

264 lines
9.3 KiB
JavaScript

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,
};