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:
@@ -72,9 +72,11 @@ function getMyPermissions(req, res) {
|
||||
for (const r of userRows) userMap[r.permission] = r.enabled === 1;
|
||||
|
||||
const defs = ALL_PERMISSIONS.filter(p => p.role === role);
|
||||
const base = {};
|
||||
defs.forEach(d => { base[d.key] = userMap[d.key] !== undefined ? userMap[d.key] : (roleMap[d.key] ?? !!d.default); });
|
||||
const result = defs.map(d => ({
|
||||
key: d.key,
|
||||
effective: userMap[d.key] !== undefined ? userMap[d.key] : (roleMap[d.key] ?? !!d.default),
|
||||
effective: base[d.key] && (d.requires || []).every(r => !!base[r]),
|
||||
}));
|
||||
res.json({ role, permissions: result });
|
||||
}
|
||||
@@ -101,13 +103,16 @@ function getUserPermissions(req, res) {
|
||||
for (const r of userRows) userMap[r.permission] = r.enabled === 1;
|
||||
|
||||
const defs = ALL_PERMISSIONS.filter(p => p.role === target.role);
|
||||
const base = {};
|
||||
defs.forEach(d => { base[d.key] = userMap[d.key] !== undefined ? userMap[d.key] : (roleMap[d.key] ?? !!d.default); });
|
||||
const result = defs.map(d => ({
|
||||
key: d.key,
|
||||
label: d.label,
|
||||
desc: d.desc,
|
||||
requires: d.requires || [],
|
||||
roleVal: roleMap[d.key] ?? d.default, // effective role-level value
|
||||
userVal: userMap[d.key], // undefined = no override
|
||||
effective: userMap[d.key] !== undefined ? userMap[d.key] : (roleMap[d.key] ?? !!d.default),
|
||||
effective: base[d.key] && (d.requires || []).every(r => !!base[r]),
|
||||
}));
|
||||
|
||||
res.json({ role: target.role, permissions: result });
|
||||
|
||||
@@ -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' });
|
||||
};
|
||||
|
||||
@@ -37,6 +37,7 @@ const PERMISSIONS = {
|
||||
role: 'teacher', roles: ['teacher'], default: 0,
|
||||
label: 'Удалять вопросы',
|
||||
desc: 'Удалять вопросы из банка (требует "Управление вопросами")',
|
||||
requires: ['questions.manage'],
|
||||
},
|
||||
'students.invite': {
|
||||
role: 'teacher', roles: ['teacher'], default: 0,
|
||||
@@ -90,6 +91,7 @@ const PERMISSIONS = {
|
||||
role: 'teacher', roles: ['teacher'], default: 0,
|
||||
label: 'Публикация шаблонов',
|
||||
desc: 'Делать свои шаблоны публичными для всех учителей',
|
||||
requires: ['templates.manage'],
|
||||
},
|
||||
'courses.manage': {
|
||||
role: 'teacher', roles: ['teacher'], default: 1,
|
||||
@@ -101,6 +103,7 @@ const PERMISSIONS = {
|
||||
role: 'teacher', roles: ['teacher'], default: 1,
|
||||
label: 'Интерактивные блоки',
|
||||
desc: 'Добавлять интерактивные задания в уроки (сопоставление, пропуски, порядок)',
|
||||
requires: ['courses.manage'],
|
||||
},
|
||||
'shop.manage': {
|
||||
role: 'teacher', roles: ['teacher'], default: 0,
|
||||
@@ -155,6 +158,7 @@ const PERMISSIONS = {
|
||||
role: 'student', roles: ['student', 'free_student'], default: 1,
|
||||
label: 'Задания в симуляциях',
|
||||
desc: 'Использовать режим "Задания" в симуляциях (квиз-режим)',
|
||||
requires: ['simulations.access'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -178,7 +182,7 @@ function listKeys() {
|
||||
function byRole(role) {
|
||||
return Object.entries(PERMISSIONS)
|
||||
.filter(([, v]) => v.role === role)
|
||||
.map(([key, v]) => ({ key, role: v.role, default: v.default, label: v.label, desc: v.desc, requireConfirmOff: !!v.requireConfirmOff }));
|
||||
.map(([key, v]) => ({ key, role: v.role, default: v.default, label: v.label, desc: v.desc, requireConfirmOff: !!v.requireConfirmOff, requires: v.requires || [] }));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user