# 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 `. `./deploy/deploy.sh up` сам вызывает `backup.sh backup --if-running`; при ошибке backup деплой прерывается. Backup на том же сервере не считается единственной защитой. - [ ] В `.env.prod` задан **`LMS_INTERNAL_TOKEN`** — длинная случайная строка (`openssl rand -hex 32`), одинаковая для identity, academic и progress. Значение по умолчанию `dev-internal-token` в production не использовать. Там же задан `LMS_DB_PASSWORD`; `.env.prod` не коммитится. - [ ] Проверить, что на сервере нет незакоммиченных изменений и выполнен `git pull` нужной ветки/тега. ### 3.2. Порядок выкладки 1. **Flyway.** `./deploy/deploy.sh up` собирает образы и запускает `postgres → identity → academic → progress → gateway → web`. Миграции применяются автоматически при старте сервисов: identity `V4–V9`, academic `V6–V11`, progress `V13`. Отдельный ручной запуск Flyway не нужен. 2. **Bootstrap курса C.** После входа админа открыть `/admin/courses`. Пока курсов нет, показан блок первичной настройки. Bootstrap идемпотентно создаёт/находит курс «Промышленное программирование на языке C», привязывает активные группы, переносит лабораторные и главы и, если лабораторных без курса не осталось, выполняет `ALTER TABLE academic.labs ALTER COLUMN course_id SET NOT NULL`. Это **post-bootstrap шаг в рантайме, а не Flyway-миграция**. Повторный запуск безопасен. Если в БД уже есть курс C, bootstrap не требуется. 3. **Postflight.** Выполнить `psql -h localhost -U lms -d lms -f deploy/preflight/groups-courses-post.sql` и проверить: - `labs_without_course = 0`; - `chapters_without_course = 0` (если все текущие материалы курсовые); - ровно один курс C; - `comments_without_author` соответствует старым комментариям; - группы без полного разбора отсутствуют. 4. **V10 → V13 — только dev/staging.** На production/master записи `progress V10__profile_authors.sql` нет, ничего удалять не нужно. Не путать с academic `V10__course_title_unique.sql` — он относится к другой схеме и должен применяться как обычно. Если БД когда-либо применяла старую feature-миграцию V10: - сделать backup; - проверить `deploy/preflight/progress-v10-v13.sql` (`columns_ok` и `index_ok` должны быть `true`); - удалить **только строку истории** V10 (`success=true`) через `flyway repair` или точечный `DELETE` из примера в скрипте; объекты схемы не удалять; - при `success=false` сначала убедиться, что частичные объекты откатились; - не переставлять `V11`/`V12` и не включать `out-of-order`. После `V13` переход forward-only. ### 3.3. Smoke после деплоя - [ ] `docker compose -f docker-compose.prod.yml --env-file .env.prod ps` — все контейнеры `running`; `curl -fsS http://127.0.0.1:8090/actuator/health` отдаёт `UP`. - [ ] **Админ:** вход, `/admin/courses` (курс C и счётчики), `/admin/groups`, `/admin/labs`, `/admin/teachers`, `/admin/promotion`. - [ ] **Преподаватель:** вход, `/teacher`, карточка своего курса, состав группы, профиль студента, скачивание попытки, курсовые материалы. - [ ] **Студент:** вход, дашборд, лабораторная, сдача/история, курсовые материалы; видны только опубликованные оценки и комментарии. - [ ] **Материалы курса:** админ и преподаватель видят скрытые статьи, студент — только опубликованные; чужой курс даёт `403`, аноним уходит на `/login`. - [ ] **Копирование лабораторной:** админ копирует лабораторную в другой курс; создаётся черновик с уровнями и вариантами; оценки, сдачи, защиты и назначения не копируются; занятый номер даёт `409`. - [ ] **Профиль:** преподаватель своего курса видит сводку, комментарии, все попытки и скачивает ZIP; чужой студент/курс — `403`. - [ ] **Повышение:** `/admin/promotion` строит dry-run за год, данные не меняются; применение — только после проверки плана; откат доступен для завершённого запуска. На production не применять план «на всякий случай». - [ ] Проверить логи `web`, `gateway`, `identity`, `academic`, `progress` на `ERROR`/`WARN` после smoke. ## 4. Известные пробелы и follow-up 1. **Активы всё ещё публичны.** Прокси `/api/v1/content/assets/{name}` (и web-прокси) не проверяет курс/статью: `article_assets` не связаны с видимостью материала. Курсовые картинки может получить любой, кто знает имя файла. Нужно использовать связь `article_assets → article → section → chapter` для проверки видимости и сделать visibility-aware маршруты, отдельный публичный путь для `ALL`. Осознанно вынесено из этапа 5, зафиксировано в [15](15-stage5-course-materials.md). 2. **Права преподавателя в основном read-only.** Кабинет преподавателя даёт обзор курса, групп, лабораторных и профилей. Авторство лабораторных и материалов, распределение вариантов, выставление оценок, комментарии, коды защиты и действия защиты остаются admin-only в UI/API. ТЗ ([07](07-groups-courses.md), раздел ролей) ожидает преподавательский self-service; это главный функциональный разрыв. 3. **Названия групп в кабинете преподавателя.** На странице курса группа показывается UUID: academic отдаёт `groupId`, а имя группы не входит в контракт карточки курса. Нужен batch-запрос имён в identity или расширение DTO. Тест `TeacherGroupTemplateTest` фиксирует fallback на id. 4. **Аудит чувствительных действий.** Есть `membership_history` (переводы, академ, отчисление, архив, повышение) и журнал `promotion_runs/items`. Отдельного audit-log для скачиваний файлов, просмотра профилей, назначений преподавателей и публикаций нет. Нужна таблица `audit_events` и запись из сервисов/BFF. 5. **Старый формат импорта не поддерживается.** Импорт принимает только шаблон v2 (поток/курс/номер/степень/год/ФИО/№/база). Ведущие нули номера группы нормализуются к числу (`01` → `1`), поэтому `ПИ-01БО` и `ПИ-1БО` неразличимы; для разных когорт нужен строковый номер. Ограничение зафиксировано в [11](11-stage1-report.md). 6. **JWT живёт до 30 дней.** Web-сессия перепроверяет статус каждые 45 секунд и закрывает UI при отчислении/архиве, но уже выданный JWT для прямого вызова API действует до истечения. Нужны короткий TTL + refresh, denylist/revocation или проверка статуса на каждом API-вызове. 7. **Нет CI.** В репозитории нет pipeline (`.github` и аналогов); гейтом служит локальный `./gradlew test ktlintCheck detekt`. Это не защищает ветку от merge без прогона и не проверяет upgrade-тест автоматически. ## 5. Рекомендуемые следующие шаги Приоритет — сверху вниз: 1. **Права преподавателя на запись.** Дать преподавателю своего курса authoring лабораторных/материалов, распределение вариантов, выставление оценок и комментариев, коды и действия защиты; закрыть negative-тестами «свой/чужой курс». Это закрывает основной разрыв с ТЗ. 2. **Видимость активов.** Использовать связь `article_assets → article → section → chapter`, проверять курс и публикацию при выдаче, оставить отдельный публичный маршрут только для `ALL`. 3. **UI авторства контента.** Выбор курса и аудитории при создании глав/ статей, чтобы преподаватель и админ не работали только через импорт; сейчас глобальные главы создаются с `ALL`, курсовые — без аудитории. 4. **Аудит.** Ввести `audit_events` для скачиваний, профилей, назначений, публикаций, повышения и жизненного цикла; экран просмотра для админа. 5. **CI.** Пайплайн на каждый push/MR: `test ktlintCheck detekt`, отдельный шаг upgrade-теста миграций на PostgreSQL 18, сборка `bootJar`; запрет merge при красном статусе. ## 6. Итог Этапы 0–5 реализованы и проверены. Security-матрица, upgrade-тест миграций и deploy-runbook этапа 6 зелёные; отчёт и чек-лист передачи готовы. Этап 6 **нельзя считать полностью завершённым** до закрытия прав преподавателя, видимости активов и CI. До этого релиз допустим только с пониманием, что преподаватели работают в режиме просмотра, а активы курсовых материалов остаются публичными.