feat(permissions): C-3 — пер-ролевые права кастомных ролей (резолвер + конфиг)

Миграция 056: снят CHECK с role_permissions.role (пересборка) → можно хранить
набор прав произвольной кастомной роли. isEnabled(uid,permRole,baseRole,key):
user override → role_permissions[customRole] → фолбэк role_permissions[base] →
дефолт реестра(base). requirePermission передаёт permRole=customRole||role.
getMyPermissions/getUserPermissions: roleMap = база + наложение кастомной роли.
Тест C-3: права кастомной роли перекрывают базу, фолбэк на базу. custom-roles 8/8,
permissions 17/17, backend без регрессий.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 15:11:56 +03:00
parent 7cdb2e2af2
commit 32c2c44b76
4 changed files with 67 additions and 19 deletions
+13 -9
View File
@@ -60,28 +60,32 @@ function requireRole(...roles) {
}
/* ── Разрешено ли ОДНО право: user override → role override → дефолт реестра ── */
function isEnabled(uid, role, key) {
// Просроченный временный оверрайд (expires_at в прошлом) игнорируем — наследуем роль.
/* Разрешено ли ОДНО право: user override (без просрочки) → role_permissions[permRole]
→ role_permissions[baseRole] (фолбэк для кастомной роли) → дефолт реестра(baseRole).
Для встроенной роли permRole === baseRole. */
function isEnabled(uid, permRole, baseRole, key) {
const userRow = db.prepare(
"SELECT enabled FROM user_permissions WHERE user_id = ? AND permission = ? AND (expires_at IS NULL OR expires_at > datetime('now'))"
).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);
let roleRow = db.prepare('SELECT enabled FROM role_permissions WHERE role = ? AND permission = ?').get(permRole, key);
if (roleRow === undefined && permRole !== baseRole) {
roleRow = db.prepare('SELECT enabled FROM role_permissions WHERE role = ? AND permission = ?').get(baseRole, key);
}
return roleRow !== undefined ? roleRow.enabled === 1 : (PERM_DEFAULTS[baseRole]?.[key] ?? false);
}
/* ── Проверка права с учётом зависимостей (requires): own AND все requires ── */
function requirePermission(key) {
return (req, res, next) => {
if (req.user?.role === 'admin') return next();
const role = req.user?.role;
const baseRole = req.user?.role;
const uid = req.user?.id;
if (!role) return res.status(401).json({ error: 'Unauthorized' });
if (!baseRole) return res.status(401).json({ error: 'Unauthorized' });
const permRole = req.user?.customRole || baseRole; // кастомная роль конфигурит права своим именем
const reqs = (registry.PERMISSIONS[key] && registry.PERMISSIONS[key].requires) || [];
const ok = isEnabled(uid, role, key) && reqs.every(r => isEnabled(uid, role, r));
const ok = isEnabled(uid, permRole, baseRole, key) && reqs.every(r => isEnabled(uid, permRole, baseRole, r));
if (ok) return next();
logDenied(req, 'perm_denied', key);
return res.status(403).json({ error: 'Permission denied' });