Files
2026-06-03 13:56:50 +03:00

16 KiB
Raw Permalink Blame History

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, middleware requirePermission (backend/src/middleware/auth.js).
  • Админ-UI: frontend/js/admin/sections/access.js (контент по классам) и permissions.js (роли).
  • Гейты: textbooks.js / exam-prep.js (router.param), отдача HTML server.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. Принципы целевой модели

  1. Два чётко разведённых понятия:
    • Способности (capabilities) — что роль/пользователь умеет делать (вести классы, модерация, покупки в магазине, управление курсами). Остаётся в role_permissions/user_permissions.
    • Видимость контента (content visibility) — что классу/ученику показывать (учебники, экзамены, курсы/теория, симуляции). Переезжает в единый content_access.
  2. Allowlist по умолчанию сохраняем (безопасно). Правило ученик > класс сохраняем.
  3. Один резолвер canAccess(user, type, ref) для всех типов контента.
  4. Целостность БД — ни одного пути, оставляющего «осиротевшие» правила.
  5. Сервер — источник правды: блокировка не должна держаться только на клиенте.

Что НЕ трогаем: ролевые «способности» (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)

  • Фаза 0purgeAccessFor + рефактор удалений + confirm bulk-close + тест content-access (commit 1bbddc0).
  • Фаза 2a — режим «Матрица» класс×контент + GET /api/access/matrix + поиск (commit 67a70c6).
  • Фаза 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: расширить CHECK content_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.