docs(access): план переработки системы прав (ревью + фазы)
Единая модель видимости контента: расширение content_access на course/sim (доступ по классам), разведение «способности (роли)» vs «видимость (классы/ ученики)», целостность (purgeAccessFor + чистка при kick), UX админки (матрица класс×контент, поиск/группировка, эффективный доступ, групповые правила по предмету/параллели), серверный гейт HTML через cookie-сессию. 4 фазы + риски. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user