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 `. `./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. До этого релиз допустим только с пониманием, что +преподаватели работают в режиме просмотра, а активы курсовых материалов +остаются публичными. diff --git a/docs/README.md b/docs/README.md index dc5d8f5..4b511bb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,6 +22,8 @@ Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid). | [12-stage2-report.md](12-stage2-report.md) | Отчёт этапа 2: курсы, bootstrap курса C, привязка и копирование лабораторных, миграция V10 | | [13-stage3-report.md](13-stage3-report.md) | Отчёт этапа 3: преподаватели, доступ к курсам, внутренние API, кабинет и профиль студента курса | | [14-stage4-report.md](14-stage4-report.md) | Отчёт этапа 4: повышение, академотпуск, отчисление, архив, восстановление и удаление | +| [15-stage5-course-materials.md](15-stage5-course-materials.md) | Отчёт этапа 5: курсовые материалы, публичные `ALL`-главы, оставшийся разрыв по активам | +| [16-stage6-report.md](16-stage6-report.md) | Отчёт этапа 6: проверка, upgrade-тест миграций, security-матрица, чек-лист деплоя и оставшиеся пробелы | | [ADR 0001](adr/0001-groups-courses-foundation.md) | Ключевые архитектурные решения: курс C, миграции, доступ, статусы, повышение | ## Кратко о системе