d9a89296de
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
16 KiB
16 KiB
PLAN — Переработка системы выдачи прав (Access / Permissions Redesign)
Составлен 2026-06-03 (Opus) по итогам ревью. Цель — свести «кто что видит» к единой связной модели, закрыть дыры целостности и безопасности, сделать админку удобной при росте числа учебников/классов/учеников. Источники-факты (проверено по коду):
backend/src/services/contentAccess.js— резолвер allowlist (учебники/экзамены).backend/src/routes/access.js— API/api/access(catalog/targets/summary/rules/class).backend/src/db/migrations/040_content_access.sql— таблицаcontent_access.backend/src/permissions/registry.js(23 ключа) +role_permissions/user_permissions, middlewarerequirePermission(backend/src/middleware/auth.js).- Админ-UI:
frontend/js/admin/sections/access.js(контент по классам) иpermissions.js(роли).- Гейты:
textbooks.js/exam-prep.js(router.param), отдача HTMLserver.js:~436/~460, клиентский редиректfrontend/js/textbook-tracker.js,frontend/js/exam-prep/common.js.
1. Текущее состояние — 2,5 параллельные системы
| Система | Регулирует | Гранулярность | Хранилище | UI |
|---|---|---|---|---|
| A. content_access | учебники, экзамены | класс + ученик (allowlist, ученик > класс) | content_access (040) |
вкладка «Доступ к учебникам» |
| B. role/user permissions | способности (вести вопросы/классы/курсы), симуляции, испытания, магазин | глобально по роли + override на пользователя | role_permissions+user_permissions (registry.js) |
вкладка «Права доступа» |
| C. курсы/теория | видимость курсов | is_published + привязка к классу |
таблицы курсов | — (нет единого места) |
Проблема: на один вопрос «кому показывать контент» отвечают три механизма и две похоже названные вкладки.
2. Принципы целевой модели
- Два чётко разведённых понятия:
- Способности (capabilities) — что роль/пользователь умеет делать (вести классы, модерация,
покупки в магазине, управление курсами). Остаётся в
role_permissions/user_permissions. - Видимость контента (content visibility) — что классу/ученику показывать (учебники,
экзамены, курсы/теория, симуляции). Переезжает в единый
content_access.
- Способности (capabilities) — что роль/пользователь умеет делать (вести классы, модерация,
покупки в магазине, управление курсами). Остаётся в
- Allowlist по умолчанию сохраняем (безопасно). Правило ученик > класс сохраняем.
- Один резолвер
canAccess(user, type, ref)для всех типов контента. - Целостность БД — ни одного пути, оставляющего «осиротевшие» правила.
- Сервер — источник правды: блокировка не должна держаться только на клиенте.
Что НЕ трогаем: ролевые «способности» (manage-права учителя, магазин) остаются по ролям — их некорректно вешать на классы.
3. Находки ревью (приоритеты)
- P0 фрагментация: теорию/симуляции нельзя открыть одному классу (только глобально по роли); путаница «Права доступа» vs «Доступ к учебникам».
- P1 HTML не гейтится на сервере:
/textbook/:slug,/exam-prep/:examKeyотдают HTML всем; блок — только клиентский редирект/403(причина: JWT вlocalStorage, не cookie). - P1 масштаб UI: плоские списки, нет поиска/группировки; «отдельные ученики»
LIMIT 500. - P2 осиротевшие per-student правила:
kickMember(classController.js:405) не чистит правила ученика. - P2 нет FK на
content_access→ чистота на ручныхDELETE. - P2 нет обзора «класс × контент» и операций «открыть весь предмет/параллель».
4. Целевая модель данных
content_access расширяется (обратносовместимо):
-- было: content_type IN ('textbook','exam')
-- станет:
content_type IN ('textbook','exam','course','sim')
content_ref -- textbook: slug хаба; exam: exam_key; course: course slug/id; sim: sim id/slug
scope IN ('class','student')
-- + опционально групповые правила (Фаза 2):
-- scope IN ('class','student','tag'), где target_ref = 'math:5' (subject:grade)
allow IN (0,1)
Резолвер contentAccess.js обобщается:
canAccess(user, type, ref)уже generic — добавитьcanAccessCourse,canAccessSimи расширитьallowedRefs/filterXна новые типы.- Порядок разрешения: личное правило ученика → правило класса (любой
allow=1) → (Фаза 2) групповое правило по тегу → default deny. - Для
sim/courseна время миграции — мост: если правил нет, читать старое ролевоеsimulations.access/публикацию, чтобы переход не отнял доступ (см. Фаза 1, миграция данных).
Прогресс (2026-06-03)
- ✅ Фаза 0 —
purgeAccessFor+ рефактор удалений + confirm bulk-close + тест content-access (commit1bbddc0). - ✅ Фаза 2a — режим «Матрица» класс×контент +
GET /api/access/matrix+ поиск (commit67a70c6). - ✅ Фаза 2b — поиск/группировка по предмету в левой колонке + бейдж «эффективный доступ» у ученика (commit
596e8d8). - ✅ Фаза 1a+1b (симуляции) — гейт + мост + админ-UI (commits
9a145e5,4549b4e). Курсы (1c) отложены. - ⏳ Фаза 1 (исходная заметка) — РЕШЕНО (пользователь): модель ДОБАВОЧНАЯ — ролевой
simulations.accessостаётся «модуль включён для роли», а видимость конкретных sim/курсов — дополнительно по классам через content_access. Эффективно:roleHasModule AND classAllowsItem. Миграция-мост открывает все sim/курсы всем классам → текущее поведение не меняется. Начинать с чтения подсистем lab/courses (где список симуляций/курсов отдаётся — туда вешать фильтр; refs: sim id/slug, course slug).
5. Фазы
Фаза 0 — Целостность и быстрые победы (низкий риск, без смены модели)
purgeAccessFor({scope, id})вservices/contentAccess.js— единая чистка правил; вызвать изdeleteClass(уже чистит — заменить на общий вызов),_deleteUserTx, и добавить вkickMember(решить: чистить ли личные правила при исключении — по умолчанию чистить только если у ученика нет других классов с этим контентом; минимально — убрать «висячие»student-deny). Зафиксировать поведение тестом.- Подтверждение для массового «Закрыть у всех / Закрыть весь» в
access.js(сейчас мгновенно). - Тех-долг БД: добавить чистящие триггеры ИЛИ оставить ручную чистку, но покрыть тестом «после удаления класса/ученика — нет строк content_access с этим target».
- Тесты: резолвер (ученик>класс, default deny), чистка при удалениях.
Фаза 1 — Единая модель видимости (расширение content_access на course/sim)
- Миграция
05X_content_access_types.sql: расширить CHECKcontent_typeдо('textbook','exam','course','sim'). Перенос данных: для каждого включённого курса/симуляции, видимых сейчас (по роли/публикации), создатьallow=1всем существующим классам (как делала 040 для учебников) — чтобы переход не отнял доступ. contentAccess.js:canAccessCourse,canAccessSim, расширитьallowedRefs/фильтры.- Гейты: подключить резолвер на роутах курсов (
GET /api/courses— сейчас гейтится толькоis_published+класс) и симуляций (/api/gamification/lab-activityи список симуляций). Ролевойsimulations.accessоставить как «способность видеть лабу вообще» ИЛИ перенести полностью в content_access (решить в начале фазы — по умолчанию: видимость по классам,simulations.accessдепрекейтим до «модуль доступен роли»). /api/access/catalog+targets+summary+rules: добавить секции «Курсы», «Симуляции».- Тесты резолвера для новых типов + миграционный тест (после переноса все классы видят то же, что и раньше).
Фаза 2 — UX админки (удобство при масштабе)
- Третий режим «Матрица»: таблица класс × контент с чекбоксами, фильтр по предмету/типу, поиск. Обзор и правка «кто что видит» одним экраном.
- Поиск + группировка по предмету в левой колонке (вместо плоских списков).
- Групповые правила (теги
subject:grade): кнопка «Открыть весь предмет / всю параллель» одной записью; резолвер учитывает теги. (Требуетscope='tag'+ UI.) - «Эффективный доступ»: для выбранного ученика/класса показать, что реально видит и почему (унаследовано от класса / личное правило / групповое) — снимает путаницу tri-state.
- Пресеты: «стартовый набор для N класса» в один клик.
- История правила из
audit(кто/когда открыл) в карточке. - Объединить вкладки под раздел «Доступ» → под-вкладки «Контент» (видимость) и «Способности» (роли). Прекращает путаницу двух похожих названий.
- Пагинация/виртуализация для «отдельных учеников» (убрать
LIMIT 500как потолок).
Фаза 3 — Серверный гейт HTML (безопасность, крупная)
- Перейти на аутентификацию через httpOnly-cookie сессию (или дублировать JWT в cookie),
чтобы
/textbook/:slugи/exam-prep/:examKeyмогли проверять доступ ПЕРЕДres.sendFile. - Middleware-гейт на отдачу HTML: нет доступа → редирект/
/403на сервере. - Сохранить клиентский редирект как запасной слой.
- Прогон полного backend-набора + ручная проверка логина/cookie во всех ролях.
- ⚠️ Затрагивает аутентификацию целиком — делать отдельной веткой, с откатом.
6. Тестирование
- Юнит резолвера
contentAccess(все типы, ученик>класс, теги, default deny). - Миграционные тесты: после 040-аналога для course/sim — прежняя видимость сохранена.
- Тесты целостности: удаление класса/ученика/исключение из класса → нет осиротевших правил.
- E2E-смоук админки по ролям (admin видит всё; teacher — только свои классы/учеников).
- pre-commit гоняет полный backend-набор (baseline 3 Auth-фейла — не трогать).
7. Риски и откат
- Фаза 1 меняет поведение симуляций/курсов → обязательный перенос данных «открыть всем классам»
- флаг отката (если что — вернуть ролевую проверку). Согласовать с пользователем ДО миграции (в памяти объединение было «отложено» — теперь решено делать: project_content_access).
- Фаза 3 трогает аутентификацию — отдельная ветка, не смешивать с контентными фазами.
8. Порядок исполнения (рекомендация)
Фаза 0 → Фаза 2 (часть: матрица/поиск/эффективный доступ — дают пользу сразу на текущей модели) → Фаза 1 (объединение типов) → Фаза 2 (групповые правила/теги, требуют новой модели) → Фаза 3.
Прогресс 2 (2026-06-03, продолжение)
- Фаза 2c ГОТОВА: массовые операции матрицы, «открыть весь предмет классу», история правил (GET /api/access/log, admin-only), пресет «копировать доступ из класса», объединение вкладок по смыслу («Доступ · контент» + «Доступ · роли»).
- Фаза 3 (серверный гейт HTML через httpOnly-cookie) — ОТЛОЖЕНА ОСОЗНАННО (низкий ROI: данные через API уже гейтятся, видно лишь пустой каркас; стоит полной переделки auth). Делать только под требование приватности/комплаенса. Ветка feature/html-access-gate.