- teacher/admin course pages, content, gradebook, profile, downloads - courseNo -> UUID только на границе REST API - обновлены e2e, шаблоны, тесты и docs - LDD-UI-002; baseline 59 claims / 385 тестов
33 KiB
16. Отчёт этапа 6: проверка, hardening и передача
Дата: 2026-10-27, обновлено 2026-10-28 после закрытия продуктовых рисков.
Ветка: feature/groups-courses.
Статус: этап 6 закрыт. Продуктовые риски (legacy-активы, срок жизни JWT,
сессии, пагинация аудита и N+1 имён групп) закрыты; оставшиеся ограничения
перечислены в разделе 4 и не блокируют передачу.
1. Итоговый объём этапов 0–6
| Этап | Что реализовано | Отчёт |
|---|---|---|
| 0 | Схема и контракты: identity V4–V13 (V10 — аудит, V11 — refresh, V12 — absolute, V13 — keyset-индексы), academic V6–V12, progress V13; preflight-скрипты; ADR; контракт профиля студента курса |
10 |
| 1 | Роли ADMIN/TEACHER/STUDENT, группы-когорты и парсер имени, статусы студентов и аккаунтов, membership_history, импорт v2 |
11 |
| 2 | Курсы, course_no для браузерных URL, история преподавателей и групп, bootstrap курса C, привязка лабораторных к курсу, копирование лабораторных, админ-UI |
12 |
| 3 | Аккаунты преподавателей, курсовая область видимости, внутренние API identity с LMS_INTERNAL_TOKEN, кабинет преподавателя, профиль студента курса и скачивание всех попыток |
13 |
| 4 | Повышение (dry-run/apply/rollback, планировщик 31 августа МСК), академотпуск, отчисление, выпуск, архив/восстановление, усиленное удаление, блокировка входа | 14 |
| 5 | Курсовые материалы /learn/course/{course_no}/..., публичные глобальные материалы ALL, скрытые материалы для преподавателя, перепроверка статуса web-сессии |
15 |
| 6 | Права преподавателя на запись (лабораторные, ведомость, группы, материалы), центральный аудит identity.audit_events, CI merge gate, hardening и deploy-runbook |
этот отчёт |
Ключевые инварианты, действующие после этапов 0–6:
- все лабораторные принадлежат курсу; существующие лабораторные, главы и активные группы перенесены в курс C;
- API работает с UUID курса, браузерные URL — с
course_no; - преподаватель видит только активные назначенные курсы; студент — только курсы активной группы; админ — все курсы;
- студент видит только опубликованные оценки и комментарии;
- активы наследуют видимость статьи → раздела → главы: курсовые требуют доступа
к курсу, глобальные — аудитории, а файлы без связи со статьёй по умолчанию
недоступны (
404) всем, кроме админа; админ перечисляет их черезGET /api/v1/content/legacy-assets; - access-токен живёт 15 минут, refresh — 7 дней с ротацией и отзывом; web-сессия скользящая на 7 дней, но не дольше абсолютного потолка 30 дней, и сама обновляет access-токен; истёкшие refresh-токены очищаются ночью;
- аудит продолжается keyset-курсором
(occurred_at, id), а детали всех лабораторных курса отдаются одним пакетным запросом без N+1; - преподаватель своего курса может писать: лабораторные/уровни/варианты, ведомость, привязка групп и авторские материалы курса; чужие курс, лаба, студент и группа отклоняются на сервисах;
- вход закрыт для отчисленных, выпущенных и архивных студентов, а также для архивных аккаунтов; web дополнительно перепроверяет статус;
- повышение и жизненный цикл пишут историю; применение повышения идемпотентно;
- чувствительные действия пишутся в центральный
identity.audit_events, админ смотрит журнал на/admin/audit; - CI на каждый push/MR прогоняет
test ktlintCheck detekt.
2. Проверка
2.1. Сборка, тесты и линтеры
Выполнено 2026-10-27:
./gradlew --no-daemon test ktlintCheck detekt
Результат: BUILD SUCCESSFUL, код возврата 0, все модули зелёные.
Для получения актуальных счётчиков тесты запущены принудительно:
./gradlew test --rerun-tasks
| Модуль | Тестов | Failures | Errors | Skipped |
|---|---|---|---|---|
| identity-service | 105 | 0 | 0 | 0 |
| academic-service | 106 | 0 | 0 | 0 |
| progress-service | 38 | 0 | 0 | 0 |
| web | 129 | 0 | 0 | 0 |
| gateway | 2 | 0 | 0 | 0 |
| markdown | 5 | 0 | 0 | 0 |
| Итого на момент этапа 6 | 385 | 0 | 0 | 0 |
После закрытия рисков актуальный LDD-baseline — 385 тестов, 0 failures
(см. раздел 7). ktlintCheck и detekt прошли без замечаний. У gateway теперь есть unit-тесты публичных auth-путей и 401; маршрутизация
gateway дополнительно проверена конфигурацией и security-матрицей ниже.
Тот же набор команд выполняет .woodpecker/ (Gitea + Woodpecker 3.x); на
каждый push/MR добавлен smoke-workflow, полный release обязателен для тегов.
2.2. Upgrade-тест миграций
Проверка выполнена на одноразовом PostgreSQL 18 (Testcontainers/контейнер, не production-БД):
- Развёрнута схема master: identity
V1–V3, academicV1–V5, progressV1–V9,V11,V12. - Загружены синтетические данные: администратор и два студента, две группы
(
ПИ-11БО,ПИ-21БО), зачисления, семестр, лабораторная, глава/раздел/ статья/актив, оценка, комментарий и попытка сдачи. - Поверх master применены миграции ветки:
- identity
V4–V13; - academic
V6–V12; - progress
V13.
- identity
- Выполнены 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;V10создаётidentity.audit_eventsбез внешних ключей и индексы поoccurred_at/action;V11создаётidentity.refresh_tokens(SHA-256-хеш,expires_at,revoked_at,replaced_by, индексы по аккаунту и сроку);V12добавляетabsolute_expires_atс бэкфиллом иNOT NULL;V13создаёт композитные индексы(occurred_at DESC, id DESC)и(action, occurred_at DESC, id DESC)под keyset-пагинацию. - 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.V12__asset_links_backfill.sqlвосстанавливает связиarticle_assetsиз markdown-ссылок/api/v1/content/assets/{name}; файлы без ссылок остаются несвязанными. Курс C миграциями не создаётся — его создаёт bootstrap после входа админа, поэтомуlabs_without_courseдо bootstrap ожидаемо больше нуля. - progress:
grade_comments.author_account_idиlab_grades.updated_by_account_idсозданы как nullableuuid; индексlab_grades_lab_idxсуществует и валиден; старый комментарий остался без автора, как и задумано. - V13 идемпотентна: повторное применение
V13(дважды) завершилось сообщениямиalready exists, skippingи не изменило схему. Это же проверяетProfileAuthorsMigrationTestна Testcontainers. - progress V10 отсутствует: в ветке нет
progress-service/.../V10__profile_authors.sql; в истории прогресса применяютсяV11,V12, затемV13. У academicV10— это другой файл,V10__course_title_unique.sql(уникальный индекс названия курса). - Переход forward-only: после
V13откат на сборку без неё не пройдёт валидацию Flyway;out-of-orderвключать нельзя.
2.3. Security-проверки
Проверено автоматическими тестами (актуальный прогон — 385 тестов, 0 failures) и разбором конфигурации gateway. Запись преподавателя дополнительно покрыта negative-тестами «свой/чужой курс».
| Сценарий | Ожидание | Факт |
|---|---|---|
| Аноним → курс/материалы курса | нет доступа | 401 на /api/v1/courses/{id}/content/tree; /api/v1/courses/by-no/{no} — 401 |
| Аноним → публичные материалы | только глобальные ALL |
публичное дерево содержит только глобальные главы с audience = ALL; курсовая глава через /api/v1/content/... — 404 |
Аноним → актив глобальной ALL-статьи |
публично | 200 |
Аноним → курсовой/STUDENTS/TEACHERS актив |
404 |
404, существование файла не раскрывается |
| Студент своего курса → актив опубликованной статьи | доступ | 200; скрытый курсовой актив — 404 |
| Студент/преподаватель чужого курса → курсовой актив | 403 |
403 |
| Преподаватель своего курса → скрытый курсовой актив | доступ | 200; админ видит любой актив |
Глобальный STUDENTS-актив → преподаватель |
403 |
403; студент получает 200 |
| Legacy-актив без связи со статьёй | 404 для анонима/студента/преподавателя, файл доступен только админу |
404 всем, кроме ADMIN; GET /api/v1/content/legacy-assets под ROLE_ADMIN перечисляет несвязанные файлы |
| Access/refresh | короткий access + ротация refresh | access JWT 15 минут (LMS_ACCESS_TOKEN_TTL), refresh 7 дней (LMS_REFRESH_TOKEN_TTL), абсолютный потолок сессии 30 дней (LMS_SESSION_ABSOLUTE_TIMEOUT); POST /api/v1/auth/refresh ротирует пару, не продлевая absolute_expires_at; повторное использование старого токена отзывает все сессии аккаунта; истёкшие токены чистит ночная задача |
| Logout / смена пароля | сессии отзываются | POST /api/v1/auth/logout отзывает refresh; смена пароля — revokeAll |
| Архив/отчисление/закрытая группа → refresh | обновление запрещено | ensureSessionAllowed при ротации: EXPELLED, GRADUATED, ARCHIVED, архивный аккаунт и GRADUATED/ARCHIVED группа дают 401 |
| Web-сессия | 7 дней sliding, без повторного входа, быстрый отзыв | cookie LMS_SESSION, Max-Age 7 дней (LMS_SESSION_TIMEOUT), HttpOnly/SameSite=Lax; access обновляется за 2 минуты до истечения, cookie перевыпускается на каждом ответе; статус перепроверяется не реже чем раз в 45 секунд, active=false или 401/403 инвалидируют сессию, недоступность identity не разлогинивает всех; выход отзывает refresh |
| Админ → курс, профиль, скачивание | полный доступ | доступ к любому курсу и студенту, скачивание любой попытки курса |
| Преподаватель своего курса → чтение | курс, группы, профиль, материалы | видит опубликованные и скрытые материалы, всех студентов активной группы курса, неопубликованные оценки и все попытки; GET /api/v1/courses/{id}/groups отдаёт привязанные группы с именами одним вызовом (без N+1) |
| Преподаватель своего курса → запись | лабы/уровни/варианты, ведомость, группы, материалы | оценки/комментарии, публикация, уровни/варианты и раздача, защиты (код/reset), привязка групп, авторство материалов; проверки в progress CourseScopeService и academic course/content authoring |
| Преподаватель чужого курса | 403 |
403 на чтение и запись: материалы, курс по course_no, состав группы, профиль, лабы, оценки, группы |
| Аудит → админ | чтение журнала | GET /api/v1/audit?limit=&action=&cursor= (offset page — для совместимости) и /admin/audit доступны только ADMIN; в UI — keyset-переход «Показать ещё», битый курсор — 400 |
| Аудит → запись из сервисов | только по токену | POST /api/v1/internal/audit с X-Internal-Token; без/неверный токен — 403 |
| Студент → свой профиль | только своё и опубликованное | скрытые оценки/комментарии отсутствуют; чужой 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/refresh/logout и публичного /api/v1/content/** |
| Gateway → заголовки личности | клиент не подменяет | X-Account-Id/Login/Role удаляются и проставляются только после проверки JWT |
Остаточные ограничения перечислены в разделе 4: ручная инвентаризация legacy-активов на production, сдачи по выбранной лабораторной в ведомости, непроверенный первый CI-прогон и отсутствие визуальной регрессии. Продуктовые риски legacy-активов, срока access-токена, пагинации, N+1 деталей лабораторных, абсолютного лимита сессии и ведущих нулей закрыты или сняты как не относящиеся к требованиям.
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нужной ветки/тега. - CI зелёный.
.woodpecker/(Gitea + Woodpecker 3.x) на каждом push/MR выполняет workflowcheck(test ktlintCheck detekt evidenceCheck) иsmoke(scripts/ldd-smoke.sh: полный стек, health, вход админа, один курс). Полныйreleaseобязателен для тегов и запускается вручную для веток. Агент — Docker backend на отдельной VM, шаги в nested dind; Java 21, Docker Compose v2, Python 3, Node и Chromium/Firefox/WebKit; установка —deploy/WOODPECKER.md. - Заданы сроки сессии:
LMS_ACCESS_TOKEN_TTL=15m,LMS_REFRESH_TOKEN_TTL=7d,LMS_SESSION_TIMEOUT=7d,LMS_SESSION_ABSOLUTE_TIMEOUT=30d,LMS_REFRESH_TOKEN_RETENTION=30d(значения по умолчанию подходят; менять только осознанно). Очистка refresh запускается поLMS_REFRESH_TOKEN_CLEANUP_CRON(по умолчанию 03:30).
3.2. Порядок выкладки
- Flyway.
./deploy/deploy.sh upсобирает образы и запускаетpostgres → identity → academic → progress → gateway → web. Миграции применяются автоматически при старте сервисов: identityV4–V13(V10—identity.audit_events,V11—identity.refresh_tokens,V12—absolute_expires_at,V13— keyset-индексы аудита), academicV6–V12, progressV13. Отдельный ручной запуск Flyway не нужен. - Bootstrap курса C. После входа админа открыть
/admin/courses. Пока курсов нет, показан блок первичной настройки. Bootstrap идемпотентно создаёт/находит курс «Промышленное программирование на языке C», привязывает активные группы, переносит лабораторные и главы и, если лабораторных без курса не осталось, выполняетALTER TABLE academic.labs ALTER COLUMN course_id SET NOT NULL. Это post-bootstrap шаг в рантайме, а не Flyway-миграция. Повторный запуск безопасен. Если в БД уже есть курс C, bootstrap не требуется. - 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соответствует старым комментариям;- группы без полного разбора отсутствуют;
- таблица
identity.audit_eventsсуществует (V10); - таблица
identity.refresh_tokensсуществует (V11); - у
refresh_tokensестьabsolute_expires_at NOT NULL(V12); - индекс
audit_events_occurred_id_idxсуществует (V13).
- V10 → V13 — только dev/staging. На production/master записи
progress V10__profile_authors.sqlнет, ничего удалять не нужно. Не путать с academicV10__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, карточка своего курса (имена групп, привязка/отвязка группы поavailable-groups), ведомость/admin/groups/{groupId}?courseNo=...без админских контролов, оценка и публикация, уровень/вариант и раздача, защита (код/reset), профиль студента, скачивание попытки и ZIP группы, авторство материалов/teacher/courses/{courseNo}/content. - Студент: вход, дашборд, лабораторная, сдача/история, курсовые материалы; видны только опубликованные оценки и комментарии.
- Материалы курса: админ и преподаватель видят скрытые статьи, студент —
только опубликованные; чужой курс даёт
403, аноним уходит на/login. - Активы: картинки курсовой статьи открываются у студента своего курса
и преподавателя; чужой курс —
403, скрытая картинка у студента —404, публичнаяALL— без входа; файл без привязки к статье —404для всех, кроме админа. Админ открывает/admin/legacy-assets(илиGET /api/v1/content/legacy-assets, илиscripts/legacy-assets.sh) и при необходимости привязывает или удаляет файлы. - Копирование лабораторной: админ копирует лабораторную в другой курс;
создаётся черновик с уровнями и вариантами; оценки, сдачи, защиты и
назначения не копируются; занятый номер даёт
409. - Профиль: преподаватель своего курса видит сводку, комментарии, все
попытки и скачивает ZIP; чужой студент/курс —
403. - Повышение:
/admin/promotionстроит dry-run за год, данные не меняются; применение — только после проверки плана; откат доступен для завершённого запуска. На production не применять план «на всякий случай». - Аудит:
/admin/auditоткрывается у админа, показывает события (например,PROFILE_VIEW,SUBMISSION_DOWNLOAD,GRADE_SET,CONTENT_*,TEACHER_*) с фильтром по действию и временем МСК; «Показать ещё» переходит по keyset-курсору (cursor), «В начало» возвращает к первой странице; преподаватель/студент получают403. - Сессия: после входа cookie
LMS_SESSIONживёт 7 дней, но не дольше 30 дней с момента входа; при работе дольше 15 минут страница продолжает работать без повторного входа (access обновляется автоматически). Выход и смена пароля требуют повторного входа; вход отчисленного/архивного студента и refresh закрытой группы отклоняются. - Проверить логи
web,gateway,identity,academic,progressнаERROR/WARNпосле smoke.
4. Известные пробелы и follow-up
- Старый формат импорта не поддерживается. Импорт принимает только шаблон v2 (поток/курс/номер/степень/год/ФИО/№/база). Ограничение зафиксировано в 11. Номер группы — числовой, поэтому отдельного требования к ведущим нулям нет.
- Инвентаризация legacy-активов на реальном хранилище. UI
/admin/legacy-assets, API иscripts/legacy-assets.shготовы; остаётся прогнать их на production/staging и привязать или удалить найденные файлы. Это гигиена хранилища, а не открытый риск доступа: обычные пользователи уже получают404. - Ведомость запрашивает сдачи по выбранной лабораторной. Детали всех
лабораторных курса теперь пакетные (
GET /api/v1/labs/details), но ведомость при открытии конкретной лабораторной делает отдельный запрос сдач. Это не N+1 по студентам и не блокер. - Browser E2E без визуальной регрессии. 6 сценариев × Chromium/Firefox/ WebKit = 18/18, но нет screenshot-diff и полного набора мобильных страниц. На Fedora WebKit запускается через Docker-образ Playwright, потому что системных Ubuntu-библиотек нет.
- Первый реальный CI-прогон не подтверждён. Конфигурация
.woodpecker/, smoke иscripts/ci-preflight.shготовы, но Woodpecker-сервер и агент на сервере ещё нужно развернуть и проверить первый push-MR pipeline и tag-job. - Внешняя интеграция «Контингент» исключена из плана. Отдельного anti-corruption слоя, синхронизации и внешних идентификаторов не будет без нового ТЗ и доступа к API.
- Mutation testing не 100%. Остаются эквивалентные мутанты и Kotlin/Spring-специфичные мутации; PIT harness — JUnit 4.
- Нагрузочный smoke — не SLA. Одна машина, синтетические данные; 125 чтений и p95 < 1 с — ориентир, а не гарантия production.
5. Рекомендуемые следующие шаги
Приоритет — сверху вниз:
- Прогнать инвентаризацию legacy-активов на production/staging через
/admin/legacy-assetsилиscripts/legacy-assets.sh, привязать нужные файлы к статьям, остальные удалить. - Развернуть Woodpecker и проверить CI. Поднять server и Docker-backend
агент на отдельной VM по
deploy/WOODPECKER.md, включить репозиторий, дождаться зелёныхcheck/smokeна push-MR иreleaseна теге. - Дальнейшее расширение E2E — визуальная регрессия и остальные мобильные страницы, если UI продолжит расти.
- Пакетные сдачи ведомости, если появятся курсы, где открытие ведомости станет заметно медленным.
6. Итог
Этап 6 закрыт: права преподавателя на запись, аудит, CI, security-матрица, upgrade-тест миграций, видимость активов и deploy-runbook проверены. Продуктовые риски закрыты: legacy-активы secure-by-default, access/refresh и web-сессия с отзывом и абсолютным потолком, keyset-пагинация аудита, пакетные детали лабораторных и bulk-выдача групп. Оставшиеся ограничения (ручная инвентаризация на production, сдачи по выбранной лабораторной, непроверенный первый CI-прогон) задокументированы в разделе 4 и не блокируют передачу.
7. LDD baseline
После этапа 6 и закрытия рисков текущая функциональность проверена через
LDD-контур: реестр docs/evidence/claims.json (59 claims, 27 P0 / 32 P1),
gate ./gradlew evidenceCheck, upgrade-скрипт, runtime-smoke, smoke-workflow,
mutation testing и браузерный Playwright E2E. Результат:
59/59 claims proven, P0 gaps = 0, 385 тестов, 0 failures; mutation strength
CourseAccessService 75%, PromotionPlanner 69%; browser E2E 18/18 (6 сценариев × Chromium/Firefox/WebKit);
конкурентные apply/rollback и масштабный нагрузочный smoke (100 групп /
1000 студентов, 125 параллельных чтений, 0 ошибок, p95 < 1 с) зелёные.
Workflow smoke выполняется на каждом push/MR, полный release обязателен
для тегов и доступен вручную для веток. E2E нашёл и закрыл реальный дефект
Jackson 3/Kotlin defaults. Подробности и остаточные ограничения —
docs/evidence/baseline-2026-10-27.md.