feat(permissions): C-1 — фундамент кастомных ролей (roles table + наследование гейтов)

Phase C, Stage C-1 (ветка feature/custom-roles): таблица roles (name, label,
base_roles JSON, is_builtin) + засев встроенных. auth.effectiveRoles(role) —
кастомная роль наследует base_roles (какие встроенные гейты проходит); встроенные
— быстрый путь без БД. requireRole() теперь проверяет пересечение allowed с
effectiveRoles → 111 существующих гейтов не задеты (встроенные ведут себя как
прежде). Дизайн: PHASE_C_DESIGN.md. Тест effectiveRoles 5/5; полный backend pass.

ВАЖНО (обнаружено): users.role в канон-схеме имеет CHECK (admin/teacher/student/
free_student), безопасно пересобрать users (FK от многих таблиц, миграции в txn)
нельзя → присвоение кастомной роли пользователю пойдёт через users.custom_role (C-2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 14:57:10 +03:00
parent a6ff965d80
commit 5aa2dd1a4b
4 changed files with 151 additions and 6 deletions
+22
View File
@@ -0,0 +1,22 @@
-- 054_roles.sql
-- Phase C (кастомные роли), Stage C-1 — реестр ролей.
-- Модель: роль наследует «базовые роли» (base_roles) — какие встроенные гейты
-- requireRole она проходит. Встроенные роли наследуют сами себя. Кастомную роль
-- админ создаёт со своим набором base_roles (доступ) и набором прав (см. C-2).
--
-- users.role хранит ИМЯ роли (CHECK на users.role нет — новое имя допустимо).
-- requireRole() расширяет роль пользователя до effectiveRoles по этой таблице.
CREATE TABLE IF NOT EXISTS roles (
name TEXT PRIMARY KEY,
label TEXT NOT NULL,
base_roles TEXT NOT NULL DEFAULT '[]', -- JSON-массив встроенных ролей, чьи гейты проходит
is_builtin INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
INSERT OR IGNORE INTO roles (name, label, base_roles, is_builtin) VALUES
('admin', 'Администратор', '["admin"]', 1),
('teacher', 'Учитель', '["teacher"]', 1),
('student', 'Ученик', '["student"]', 1),
('free_student', 'Свободный ученик', '["free_student","student"]', 1);