From edb98895dff540ad261b89e8805be8ed47f5f19d Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Wed, 3 Jun 2026 12:32:00 +0300 Subject: [PATCH] =?UTF-8?q?docs(access):=20=D0=BF=D0=BB=D0=B0=D0=BD=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D1=8B=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=20(=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20+=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B7=D1=8B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Единая модель видимости контента: расширение content_access на course/sim (доступ по классам), разведение «способности (роли)» vs «видимость (классы/ ученики)», целостность (purgeAccessFor + чистка при kick), UX админки (матрица класс×контент, поиск/группировка, эффективный доступ, групповые правила по предмету/параллели), серверный гейт HTML через cookie-сессию. 4 фазы + риски. Co-Authored-By: Claude Opus 4.8 (1M context) --- plans/access-redesign/PLAN.md | 151 ++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 plans/access-redesign/PLAN.md diff --git a/plans/access-redesign/PLAN.md b/plans/access-redesign/PLAN.md new file mode 100644 index 0000000..7bfcbe1 --- /dev/null +++ b/plans/access-redesign/PLAN.md @@ -0,0 +1,151 @@ +# 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` расширяется (обратносовместимо): + +```sql +-- было: 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, миграция данных). + +--- + +## 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.