feat(permissions): A1 — зависимости между правами (requires) + план переработки

registry: поле requires (questions.delete→manage, templates.public→manage,
courses.interactive→manage, simulations.quiz→access), проброшено в byRole.
auth.requirePermission: вынесен isEnabled(); право = own AND все requires
(дочернее не работает без родителя). /me и /users/🆔 effective с учётом
requires + requires в ответе. UI permissions.js: каскад — дочернее с
невыполненной зависимостью неактивно (тумблер заблокирован + «Требует: …»).
Тест зависимости. План: plans/permissions-rework/PLAN.md. Backend 216 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 14:10:20 +03:00
parent e37432d812
commit 9ac2a612e0
6 changed files with 117 additions and 24 deletions
+16 -17
View File
@@ -42,7 +42,19 @@ function requireRole(...roles) {
};
}
/* ── Check permission; user override → role override → hardcoded default ── */
/* ── Разрешено ли ОДНО право: user override → role override → дефолт реестра ── */
function isEnabled(uid, role, key) {
const userRow = db.prepare(
'SELECT enabled FROM user_permissions WHERE user_id = ? AND permission = ?'
).get(uid, key);
if (userRow !== undefined) return userRow.enabled === 1;
const roleRow = db.prepare(
'SELECT enabled FROM role_permissions WHERE role = ? AND permission = ?'
).get(role, key);
return roleRow !== undefined ? roleRow.enabled === 1 : (PERM_DEFAULTS[role]?.[key] ?? false);
}
/* ── Проверка права с учётом зависимостей (requires): own AND все requires ── */
function requirePermission(key) {
return (req, res, next) => {
if (req.user?.role === 'admin') return next();
@@ -50,22 +62,9 @@ function requirePermission(key) {
const uid = req.user?.id;
if (!role) return res.status(401).json({ error: 'Unauthorized' });
// 1. User-level override
const userRow = db.prepare(
'SELECT enabled FROM user_permissions WHERE user_id = ? AND permission = ?'
).get(uid, key);
if (userRow !== undefined) {
if (userRow.enabled === 1) return next();
logDenied(req, 'perm_denied', key);
return res.status(403).json({ error: 'Permission denied' });
}
// 2. Role-level
const roleRow = db.prepare(
'SELECT enabled FROM role_permissions WHERE role = ? AND permission = ?'
).get(role, key);
const enabled = roleRow !== undefined ? roleRow.enabled === 1 : (PERM_DEFAULTS[role]?.[key] ?? false);
if (enabled) return next();
const reqs = (registry.PERMISSIONS[key] && registry.PERMISSIONS[key].requires) || [];
const ok = isEnabled(uid, role, key) && reqs.every(r => isEnabled(uid, role, r));
if (ok) return next();
logDenied(req, 'perm_denied', key);
return res.status(403).json({ error: 'Permission denied' });
};