diff --git a/docs/08-groups-courses-plan.md b/docs/08-groups-courses-plan.md index c3ba65f..1505d74 100644 --- a/docs/08-groups-courses-plan.md +++ b/docs/08-groups-courses-plan.md @@ -1,6 +1,9 @@ # 08. План реализации: группы, курсы, преподаватели -**Статус:** план согласован, этапы 0–4 выполнены, этапы 5–6 — в работе. +**Статус:** план согласован, этапы 0–5 выполнены. Проверка этапа 6 выполнена +(тесты, upgrade миграций, security, deploy-runbook), но этап 6 закрыт не +полностью: остаются права преподавателя на запись, видимость активов и CI — +см. § 9.1 и [16-stage6-report.md](16-stage6-report.md). **Основа:** [07-groups-courses.md](07-groups-courses.md). **Дата решений:** 2026-10-23. @@ -311,12 +314,35 @@ identity V9, **academic V11**, progress V13 (+V14 при необходимос 31 августа МСК и догон), жизненный цикл студента, архив группы, усиленное подтверждение удаления, блокировка входа для архивных и выпущенных групп. Отчёт: [14-stage4-report.md](14-stage4-report.md). -- **Этап 5 — остаётся.** UX и контент: студенческие страницы курсов, - материалы по `/learn/course/{course_no}/...` и аудитории, наследуемая - видимость активов, публичные материалы `ALL`. -- **Этап 6 — остаётся.** Hardening: полная security-матрица, - upgrade-тест миграций, аудит, индексы/пагинация, backup/restore, - документация и smoke. +- **Этап 5 — выполнен.** Студенческие страницы курсов, материалы по + `/learn/course/{course_no}/...` и аудитории, публичные материалы `ALL`, + скрытые материалы для преподавателя, перепроверка статуса web-сессии. + Отчёт: [15-stage5-course-materials.md](15-stage5-course-materials.md). +- **Этап 6 — проверка выполнена, закрыт не полностью.** Проверены полный + прогон тестов/линтеров, upgrade миграций с master-схемы, security-матрица, + gateway и deploy-runbook. Отчёт: [16-stage6-report.md](16-stage6-report.md). + Не закрыты: права преподавателя на запись, видимость активов, аудит + скачиваний и CI. Остальные пункты hardening (индексы, backup/restore, + документация, smoke) описаны в отчёте и чек-листе. + +### 9.1. Известные пробелы после этапа 6 + +- **Активы публичны.** `/api/v1/content/assets/{name}` не проверяет курс и + статью; нужна проверка `article_assets → article → section → chapter` и + visibility-aware маршруты. +- **Преподаватель — в основном read-only.** Авторство лабораторных/материалов, + распределение, оценки, комментарии и защита остаются admin-only в UI/API; + ТЗ ожидает преподавательский self-service. +- **Имена групп в кабинете преподавателя.** На странице курса показывается + UUID группы, batch-разрешения имён нет. +- **Аудит.** Есть история членства и журнал повышения, но нет audit-log + скачиваний, просмотров профилей, назначений и публикаций. +- **Импорт.** Старый формат не поддерживается; ведущие нули номера группы + нормализуются (`01` → `1`). +- **JWT 30 дней.** Web перепроверяет статус каждые 45 секунд, но прямой API + с уже выданным токеном живёт до истечения. +- **CI отсутствует.** Гейт — локальный `./gradlew test ktlintCheck detekt`; + автоматического pipeline и upgrade-теста в CI нет. ## 10. Definition of Done diff --git a/docs/16-stage6-report.md b/docs/16-stage6-report.md new file mode 100644 index 0000000..ec40a34 --- /dev/null +++ b/docs/16-stage6-report.md @@ -0,0 +1,271 @@ +# 16. Отчёт этапа 6: проверка, hardening и передача + +**Дата:** 2026-10-27. +**Ветка:** `feature/groups-courses`. +**Статус:** проверка этапа 6 выполнена; этап 6 закрыт **не полностью**. +Миграции, security-матрица, upgrade-тест и deploy-runbook проверены. Открытыми +остаются права преподавателя на запись, видимость активов, аудит и CI — см. +раздел 4. + +## 1. Итоговый объём этапов 0–5 + +| Этап | Что реализовано | Отчёт | +|---|---|---| +| 0 | Схема и контракты: identity `V4–V9`, academic `V6–V11`, progress `V13`; preflight-скрипты; ADR; контракт профиля студента курса | [10](10-stage0-report.md) | +| 1 | Роли `ADMIN/TEACHER/STUDENT`, группы-когорты и парсер имени, статусы студентов и аккаунтов, `membership_history`, импорт v2 | [11](11-stage1-report.md) | +| 2 | Курсы, `course_no` для браузерных URL, история преподавателей и групп, bootstrap курса C, привязка лабораторных к курсу, копирование лабораторных, админ-UI | [12](12-stage2-report.md) | +| 3 | Аккаунты преподавателей, курсовая область видимости, внутренние API identity с `LMS_INTERNAL_TOKEN`, кабинет преподавателя, профиль студента курса и скачивание всех попыток | [13](13-stage3-report.md) | +| 4 | Повышение (dry-run/apply/rollback, планировщик 31 августа МСК), академотпуск, отчисление, выпуск, архив/восстановление, усиленное удаление, блокировка входа | [14](14-stage4-report.md) | +| 5 | Курсовые материалы `/learn/course/{course_no}/...`, публичные глобальные материалы `ALL`, скрытые материалы для преподавателя, перепроверка статуса web-сессии | [15](15-stage5-course-materials.md) | + +Ключевые инварианты, действующие после этапов 0–5: + +- все лабораторные принадлежат курсу; существующие лабораторные, главы и + активные группы перенесены в курс C; +- API работает с UUID курса, браузерные URL — с `course_no`; +- преподаватель видит только активные назначенные курсы; студент — только курсы + активной группы; админ — все курсы; +- студент видит только опубликованные оценки и комментарии; +- вход закрыт для отчисленных, выпущенных и архивных студентов, а также для + архивных аккаунтов; web дополнительно перепроверяет статус; +- повышение и жизненный цикл пишут историю; применение повышения идемпотентно. + +## 2. Проверка + +### 2.1. Сборка, тесты и линтеры + +Выполнено 2026-10-27: + +```bash +./gradlew test ktlintCheck detekt +``` + +Результат: `BUILD SUCCESSFUL`, код возврата `0`, все модули зелёные. + +Для получения актуальных счётчиков тесты запущены принудительно: + +```bash +./gradlew test --rerun-tasks +``` + +| Модуль | Тестов | Failures | Errors | Skipped | +|---|---:|---:|---:|---:| +| identity-service | 62 | 0 | 0 | 0 | +| academic-service | 53 | 0 | 0 | 0 | +| web | 40 | 0 | 0 | 0 | +| progress-service | 12 | 0 | 0 | 0 | +| markdown | 5 | 0 | 0 | 0 | +| **Итого** | **172** | **0** | **0** | **0** | + +`ktlintCheck` и `detekt` прошли без замечаний. Отдельных unit-тестов у gateway +и `common-http` в текущем дереве нет; маршрутизация gateway проверена +конфигурацией и security-матрицей ниже. + +### 2.2. Upgrade-тест миграций + +Проверка выполнена на одноразовом PostgreSQL 18 (Testcontainers/контейнер, +не production-БД): + +1. Развёрнута схема master: identity `V1–V3`, academic `V1–V5`, + progress `V1–V9`, `V11`, `V12`. +2. Загружены синтетические данные: администратор и два студента, две группы + (`ПИ-11БО`, `ПИ-21БО`), зачисления, семестр, лабораторная, глава/раздел/ + статья/актив, оценка, комментарий и попытка сдачи. +3. Поверх master применены миграции ветки: + - identity `V4–V9`; + - academic `V6–V11`; + - progress `V13`. +4. Выполнены postflight-запросы и повторное применение `V13`. + +Результат: **успешно, без ошибок**. + +- **identity:** группы разобраны в `stream/course/number/degree/enrollment_year` + (`ПИ-11БО` → поток `ПИ`, курс 1, номер 1, `БО`, год набора 2026; `ПИ-21БО` → + курс 2, год 2025); статус `ACTIVE`; созданы `promotion_runs`, + `promotion_run_items`; зачисления перенесены в `membership_history` + действием `ENROLL`; добавлен `accounts.status`. +- **academic:** добавлены `labs.course_id`, `chapters.course_id/audience`, + индексы `labs_number_course_uidx`, `chapters_slug_global_uidx`, + `chapters_slug_course_uidx`, `article_assets_article_idx`, + `courses_title_lower_uidx`. Курс C миграциями не создаётся — его создаёт + bootstrap после входа админа, поэтому `labs_without_course` до bootstrap + ожидаемо больше нуля. +- **progress:** `grade_comments.author_account_id` и + `lab_grades.updated_by_account_id` созданы как nullable `uuid`; индекс + `lab_grades_lab_idx` существует и валиден; старый комментарий остался без + автора, как и задумано. +- **V13 идемпотентна:** повторное применение `V13` (дважды) завершилось + сообщениями `already exists, skipping` и не изменило схему. Это же + проверяет `ProfileAuthorsMigrationTest` на Testcontainers. +- **progress V10 отсутствует:** в ветке нет + `progress-service/.../V10__profile_authors.sql`; в истории прогресса + применяются `V11`, `V12`, затем `V13`. У academic `V10` — это другой файл, + `V10__course_title_unique.sql` (уникальный индекс названия курса). +- Переход **forward-only**: после `V13` откат на сборку без неё не пройдёт + валидацию Flyway; `out-of-order` включать нельзя. + +### 2.3. Security-проверки + +Проверено автоматическими тестами (полный прогон 172 теста) и разбором +конфигурации gateway. + +| Сценарий | Ожидание | Факт | +|---|---|---| +| Аноним → курс/материалы курса | нет доступа | `401` на `/api/v1/courses/{id}/content/tree`; `/api/v1/courses/by-no/{no}` — `401` | +| Аноним → публичные материалы | только глобальные `ALL` | публичное дерево содержит только глобальные главы с `audience = ALL`; курсовая глава через `/api/v1/content/...` — `404` | +| Админ → курс, профиль, скачивание | полный доступ | доступ к любому курсу и студенту, скачивание любой попытки курса | +| Преподаватель своего курса | курс, группы, профиль, материалы | видит опубликованные и скрытые материалы, всех студентов активной группы курса, неопубликованные оценки и все попытки | +| Преподаватель чужого курса | `403` | `403` на материалы, курс по `course_no`, состав группы и профиль | +| Студент → свой профиль | только своё и опубликованное | скрытые оценки/комментарии отсутствуют; чужой `studentId` — `403` | +| Студент → чужой курс/группа | `403` | доступ только к курсам активной группы; архивный курс — `403` | +| Чужая сдача / лаба другого курса | `403` | скачивание отклоняется; своя попытка курса скачивается ZIP | +| Внутренние API identity | только по токену | неверный/пустой `X-Internal-Token` — `403`, сравнение constant-time; неизвестный id — `404` | +| Gateway | внутренние пути не публикуются | маршрутов на `/api/v1/internal/**` нет; gateway требует JWT для всего, кроме `/actuator`, входа/setup/JWKS и публичного `/api/v1/content/**` | +| Gateway → заголовки личности | клиент не подменяет | `X-Account-Id/Login/Role` удаляются и проставляются только после проверки JWT | +| Web-сессия | быстрый отзыв доступа | статус перепроверяется не реже чем раз в 45 секунд; `active=false` или `401/403` инвалидируют сессию; недоступность identity не разлогинивает всех | + +Остаточные риски перечислены в разделе 4: публичные активы и 30-дневный JWT +для прямого API. + +## 3. Чек-лист деплоя + +### 3.1. Перед деплоем + +- [ ] **Backup обязателен.** Выполнить `./deploy/backup.sh backup` и + `./deploy/backup.sh verify