- 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
264 lines
9.3 KiB
JavaScript
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,
|
|
};
|