docs: итоговый отчёт и оставшиеся разрывы

This commit is contained in:
2026-10-27 19:21:48 +03:00
parent fba648ea54
commit 3fd0d41cf2
3 changed files with 306 additions and 7 deletions

View File

@@ -1,6 +1,9 @@
# 08. План реализации: группы, курсы, преподаватели # 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). **Основа:** [07-groups-courses.md](07-groups-courses.md).
**Дата решений:** 2026-10-23. **Дата решений:** 2026-10-23.
@@ -311,12 +314,35 @@ identity V9, **academic V11**, progress V13 (+V14 при необходимос
31 августа МСК и догон), жизненный цикл студента, архив группы, 31 августа МСК и догон), жизненный цикл студента, архив группы,
усиленное подтверждение удаления, блокировка входа для архивных усиленное подтверждение удаления, блокировка входа для архивных
и выпущенных групп. Отчёт: [14-stage4-report.md](14-stage4-report.md). и выпущенных групп. Отчёт: [14-stage4-report.md](14-stage4-report.md).
- **Этап 5 — остаётся.** UX и контент: студенческие страницы курсов, - **Этап 5 — выполнен.** Студенческие страницы курсов, материалы по
материалы по `/learn/course/{course_no}/...` и аудитории, наследуемая `/learn/course/{course_no}/...` и аудитории, публичные материалы `ALL`,
видимость активов, публичные материалы `ALL`. скрытые материалы для преподавателя, перепроверка статуса web-сессии.
- **Этап 6 — остаётся.** Hardening: полная security-матрица, Отчёт: [15-stage5-course-materials.md](15-stage5-course-materials.md).
upgrade-тест миграций, аудит, индексы/пагинация, backup/restore, - **Этап 6 — проверка выполнена, закрыт не полностью.** Проверены полный
документация и smoke. прогон тестов/линтеров, 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 ## 10. Definition of Done

271
docs/16-stage6-report.md Normal file
View File

@@ -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 <dir>`. `./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. До этого релиз допустим только с пониманием, что
преподаватели работают в режиме просмотра, а активы курсовых материалов
остаются публичными.

View File

@@ -22,6 +22,8 @@ Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid).
| [12-stage2-report.md](12-stage2-report.md) | Отчёт этапа 2: курсы, bootstrap курса C, привязка и копирование лабораторных, миграция V10 | | [12-stage2-report.md](12-stage2-report.md) | Отчёт этапа 2: курсы, bootstrap курса C, привязка и копирование лабораторных, миграция V10 |
| [13-stage3-report.md](13-stage3-report.md) | Отчёт этапа 3: преподаватели, доступ к курсам, внутренние API, кабинет и профиль студента курса | | [13-stage3-report.md](13-stage3-report.md) | Отчёт этапа 3: преподаватели, доступ к курсам, внутренние API, кабинет и профиль студента курса |
| [14-stage4-report.md](14-stage4-report.md) | Отчёт этапа 4: повышение, академотпуск, отчисление, архив, восстановление и удаление | | [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, миграции, доступ, статусы, повышение | | [ADR 0001](adr/0001-groups-courses-foundation.md) | Ключевые архитектурные решения: курс C, миграции, доступ, статусы, повышение |
## Кратко о системе ## Кратко о системе