docs: закрытие продуктовых рисков и новый LDD baseline

This commit is contained in:
2026-10-28 13:26:43 +03:00
parent 71fa3284a0
commit 9dcba8ce0c
16 changed files with 584 additions and 212 deletions

View File

@@ -54,7 +54,8 @@ progress `:8083`, web `:8090`. Интерфейс: `http://localhost:8090`.
./gradlew ktlintCheck detekt test # линтеры (строгий режим) + тесты
./gradlew evidenceCheck # LDD: P0-claims должны иметь зелёные свидетельства
./gradlew ktlintFormat # автоформат Kotlin
./scripts/ldd-runtime.sh # runtime-smoke полного стека на одноразовой БД
./scripts/ldd-smoke.sh # быстрый smoke полного стека (как CI job smoke)
./scripts/ldd-runtime.sh # runtime-smoke полного стека + scale load
./scripts/ldd-migration-upgrade.sh # upgrade с master-схемы
./scripts/ldd-mutation.sh # PIT: mutation strength критичных P0-компонентов
./scripts/ldd-browser.sh # Playwright E2E (cd e2e && npm install && npx playwright install chromium)
@@ -69,10 +70,14 @@ progress `:8083`, web `:8090`. Интерфейс: `http://localhost:8090`.
- Тестовая инфраструктура — `libs:test-support` (JUnit 5, MockK, Testcontainers `postgres:18`).
- **LDD** — реестр проверяемых утверждений `docs/evidence/claims.json`;
P0 без зелёного теста валит `./gradlew evidenceCheck`. Runtime, migration,
mutation и browser E2E — отдельные скрипты из `scripts/`; baseline —
mutation, browser E2E и smoke — скрипты из `scripts/`; baseline —
`docs/evidence/baseline-2026-10-27.md`.
- CI merge gate — `.gitlab-ci.yml`: `./gradlew --no-daemon test ktlintCheck detekt evidenceCheck`
на push/MR. Runner должен разрешать privileged `docker:dind` для Testcontainers.
- CI merge gate — `.gitlab-ci.yml`: `test ktlintCheck detekt evidenceCheck` и
job `smoke` (`scripts/ldd-smoke.sh`) на каждом push/MR. Полный
`release-evidence` (`scripts/ldd-release.sh`) обязателен для тегов и
запускается вручную для веток. `check` требует privileged `docker:dind`
для Testcontainers; `smoke`/`release-evidence` — shell-runner с Java 21,
Docker Compose v2, Python 3 и (для release) Node/Chromium.
## Запуск в Docker (для сервера)
@@ -113,6 +118,11 @@ cp .env.prod.example .env.prod # заполнить LMS_DB_PASSWORD
- **Доступ**: JWT (RSA, ключи identity генерирует при первом старте и хранит в БД),
проверка токена на gateway, роли ADMIN/TEACHER/STUDENT, CSRF на всех POST-формах,
session fixation-защита; вход студента с обязательной сменой пароля.
Access-токен живёт 15 минут (`LMS_ACCESS_TOKEN_TTL`), refresh — 7 дней
(`LMS_REFRESH_TOKEN_TTL`) с ротацией и отзывом; прямой API использует
`POST /api/v1/auth/refresh`. Web-сессия `LMS_SESSION` — 7 дней sliding
(`LMS_SESSION_TIMEOUT`), access обновляется автоматически за 2 минуты до
истечения; logout и смена пароля отзывают refresh.
- **Группы и студенты**: группы-когорты (поток, курс, номер, степень, год набора),
импорт `.xls/.xlsx/.csv` по шаблону `поток/курс/номер/степень/год набора/ФИО/№/база`,
ручное добавление студента (генерация пароля) и «Добавить существующего»
@@ -139,8 +149,8 @@ cp .env.prod.example .env.prod # заполнить LMS_DB_PASSWORD
`/teacher/courses/{id}/content`. Чужой курс отклоняется на сервисах.
- **Аудит**: чувствительные действия (профили, скачивания, оценки, защиты,
курсы, группы, материалы, преподаватели, жизненный цикл) пишутся в
`identity.audit_events`; админ смотрит `/admin/audit` с фильтром по действию
и временем МСК.
`identity.audit_events`; админ смотрит `/admin/audit` с фильтром по действию,
временем МСК и постраничным переходом «Показать ещё» (`page`/`limit`).
- **Повышение и жизненный цикл**: `/admin/promotion` — dry-run, применение
и откат повышения (автоматический dry-run 31 августа МСК); на странице
группы — академотпуск, возврат, отчисление, архив и восстановление;
@@ -165,6 +175,9 @@ cp .env.prod.example .env.prod # заполнить LMS_DB_PASSWORD
markdown (код, спойлеры, картинки; санитизация OWASP); импорт ZIP
(`chapters/N-глава/N-раздел/N-статья.md` + `assets/`); создание/переименование/
удаление глав, разделов, статей; перенос статей между разделами.
Видимость картинок наследуется от статьи; файл без привязки к статье отдаёт
`404` обычным пользователям и доступен только админу, который получает
список таких файлов через `GET /api/v1/content/legacy-assets`.
- **Студент**: дашборд успеваемости (сдано, средний балл, лучшая/слабая оценка),
карточки опубликованных лаб, страница лабы: условие, свой вариант, сдача
файлов (zip/.c, переотправка до дедлайна, история, скачивание последней),

View File

@@ -57,6 +57,22 @@ Compose передаёт одно и то же значение identity, academ
Значение по умолчанию `dev-internal-token` для production не используйте;
`.env.prod` не коммитьте.
Сроки аутентификации и сессии (можно не задавать — действуют значения ниже):
```dotenv
LMS_ACCESS_TOKEN_TTL=15m # access JWT identity, короткий
LMS_REFRESH_TOKEN_TTL=7d # refresh-токен, ротация и отзыв
LMS_SESSION_TIMEOUT=7d # web-сессия LMS_SESSION, sliding
```
`LMS_ACCESS_TOKEN_TTL` и `LMS_REFRESH_TOKEN_TTL` читает identity,
`LMS_SESSION_TIMEOUT` — web. `docker-compose.prod.yml` прокидывает эти
переменные в environment соответствующих сервисов; при переопределении
используются значения из `.env.prod`. Access
обновляется автоматически за 2 минуты до истечения; прямой API-клиент должен
сам вызывать `POST /api/v1/auth/refresh`. Уменьшайте значения только осознанно:
слишком короткая web-сессия заставит пользователей чаще входить заново.
## 4. Собрать и запустить
```bash
@@ -109,16 +125,24 @@ git pull
`deploy.sh up` **обязательно** делает backup перед обновлением. Если backup
завершился ошибкой, деплой прерывается. Миграции БД (Flyway) применяются
автоматически при старте сервисов: identity `V4–V10` (`V10` создаёт
`identity.audit_events`), academic `V6–V12`, progress `V13`.
автоматически при старте сервисов: identity `V4–V11` (`V10` создаёт
`identity.audit_events`, `V11` — `identity.refresh_tokens`), academic `V6–V12`,
progress `V13`.
После обновления проверьте, что админ открывает `/admin/audit` и видит события
(например, после входа в профиль студента — `PROFILE_VIEW`); преподаватель и
студент на эту страницу не попадают.
студент на эту страницу не попадают. Журнал листается через `page`/`limit`
(«Показать ещё»). Для инвентаризации старых файлов админ может вызвать
`GET /api/v1/content/legacy-assets`: файлы без привязки к статье больше не
публичны (`404` для обычных пользователей), их нужно привязать или удалить.
CI (`.gitlab-ci.yml`) на каждом push/MR выполняет
`./gradlew --no-daemon test ktlintCheck detekt`; для Testcontainers runner
должен разрешать privileged `docker:dind`.
`./gradlew --no-daemon test ktlintCheck detekt evidenceCheck` и job `smoke`
(`./scripts/ldd-smoke.sh`: полный стек, health, вход админа, один курс).
Полный job `release-evidence` (`./scripts/ldd-release.sh`) обязателен для тегов
и запускается вручную для веток. `check` использует privileged `docker:dind`
для Testcontainers; `smoke`/`release-evidence` выполняются на shell-runner
с Java 21, Docker Compose v2, Python 3, Node и Chromium Playwright.
## 8. Резервное копирование

View File

@@ -32,8 +32,11 @@ flowchart LR
id -.->|JWKS| pr
```
- Браузер никогда не получает JWT: токен живёт в HTTP-сессии web-сервиса
(`lms.token`), а в браузер уходит только cookie сессии (`HttpOnly`, 30 дней).
- Браузер никогда не получает JWT: access-токен живёт в HTTP-сессии web-сервиса
(`lms.token`), а в браузер уходит только cookie сессии `LMS_SESSION`
(`HttpOnly`, `SameSite=Lax`, 7 дней sliding). Access-токен — 15 минут,
обновляется автоматически за 2 минуты до истечения по refresh-токену;
cookie `Max-Age` перевыпускается на каждом ответе.
- Все REST-запросы web проксирует на gateway, добавляя `Authorization: Bearer`.
- Gateway проверяет подпись токена по JWKS identity и передаёт вниз заголовки
`X-Account-Id`, `X-Account-Login`, `X-Account-Role`.
@@ -139,12 +142,13 @@ flowchart TD
n -->|да| p["Контроллер → сервис → БД"]
```
- Роли: `ADMIN`, `STUDENT`; конвертер ролей читает claim `role` как `ROLE_<role>`.
- Публичные пути: `/actuator/**`, `/api/v1/auth/{health,setup,login,jwks.json}`,
- Роли: `ADMIN`, `TEACHER`, `STUDENT`; конвертер ролей читает claim `role` как `ROLE_<role>`.
- Публичные пути: `/actuator/**`, `/api/v1/auth/{health,setup,login,refresh,jwks.json}`,
`/api/v1/content/tree`, `/api/v1/content/{chapter}/{section}/{article}`.
`/api/v1/content/assets/**` gateway пропускает без JWT (legacy/публичные
`/api/v1/content/assets/**` gateway пропускает без JWT (публичные
`ALL`-активы), но academic проверяет видимость статьи и для курсовых или
ролевых активов требует токен.
ролевых активов требует токен. Файл без привязки к статье — `404` всем,
кроме админа (secure-by-default).
- RSA-ключи identity хранятся в таблице `settings` и публикуются в JWKS.
## 5. Рендеринг страниц и защита форм (web)

View File

@@ -52,6 +52,10 @@ flowchart TB
`LMS_JWT_JWK_SET_URI` обязателен для gateway, academic и progress —
в контейнерах это `http://identity:8081/api/v1/auth/jwks.json` (иначе 401).
Сроки сессии задаются переменными (значения по умолчанию подходят):
`LMS_ACCESS_TOKEN_TTL=15m` и `LMS_REFRESH_TOKEN_TTL=7d` для identity,
`LMS_SESSION_TIMEOUT=7d` для web.
## 3. Сборка
```mermaid
@@ -101,7 +105,7 @@ sequenceDiagram
DC->>DC: docker build (5 стадий)
DC->>PG: старт postgres (volume lms_pgdata)
DC->>DC: старт identity → academic → progress → gateway → web
Note over DC: Flyway применяет миграции автоматически<br/>при старте сервисов (identity V1..V10, academic V1..V12, progress V1..V9 и V11..V13; у web миграций нет)
Note over DC: Flyway применяет миграции автоматически<br/>при старте сервисов (identity V1..V11, academic V1..V12, progress V1..V9 и V11..V13; у web миграций нет)
```
- Локально (`docker-compose.yml`) поднимается только `postgres`; прод-стек — `docker-compose.prod.yml`.

View File

@@ -128,7 +128,9 @@ printf("Ответ: 42\n");
* Ссылки вида `assets/имя` автоматически заменяются на адрес
`/api/v1/content/assets/...`; сами файлы складываются в хранилище. Для
курсовых статей этот адрес требует сессию пользователя курса, для публичных
`ALL`-материалов и старых несвязанных файлов остаётся публичным.
`ALL`-материалов остаётся публичным. Файл без привязки к статье больше не
публичен: обычные пользователи получают `404`, админ — файл и его имя в
`GET /api/v1/content/legacy-assets` для привязки или удаления.
## Публикация и скрытие

View File

@@ -60,8 +60,8 @@
**Разбор имени группы:** `^(.+)-(\d)(\d+)(БО|МО)$` → `stream` (буквы/дефисы), `course` (одна цифра),
`number` (остальные цифры), `degree`. Примеры: `ПИ-11БО` → ПИ/1/1/БО; `ИВТ-ТОП-12БО` → ИВТ-ТОП/1/2/БО;
`ПИ-101БО` → курс 1, номер 01 (ведущие нули допустимы). Разбор выполняется при создании/импорте,
далее живём по полям; имя храним для отображения/выгрузок.
`ПИ-101БО` → курс 1, номер 1. `number` — числовой; ведущий ноль не является отдельным значением.
Разбор выполняется при создании/импорте, далее живём по полям; имя храним для отображения/выгрузок.
---
@@ -227,7 +227,29 @@ CREATE INDEX audit_events_action_idx ON identity.audit_events (action);
```
Внешних ключей нет: событие должно переживать удаление субъекта. Запись идёт
из сервисов через `POST /api/v1/internal/audit` (`X-Internal-Token`), чтение —
`GET /api/v1/audit` и `/admin/audit` только для админа.
`GET /api/v1/audit?page=&limit=&action=` и `/admin/audit` только для админа.
Страница аудита поддерживает `page` (с нуля) и `limit` (максимум 500), в UI —
переход «Показать ещё».
**V11 — refresh-токены (добавлена при закрытии рисков аутентификации)**
```sql
CREATE TABLE identity.refresh_tokens (
id uuid PRIMARY KEY,
account_id uuid NOT NULL REFERENCES identity.accounts(id) ON DELETE CASCADE,
token_hash varchar(64) NOT NULL UNIQUE,
issued_at timestamptz NOT NULL,
expires_at timestamptz NOT NULL,
revoked_at timestamptz,
replaced_by uuid
);
CREATE INDEX refresh_tokens_account_idx ON identity.refresh_tokens (account_id);
CREATE INDEX refresh_tokens_expires_idx ON identity.refresh_tokens (expires_at);
```
Хранится только SHA-256-хеш raw-токена. Ротация при `POST /api/v1/auth/refresh`
помечает старый токен заменённым; повторное использование заменённого токена
трактуется как кража и отзывает все сессии аккаунта. `logout` и смена пароля
отзывают refresh; отчисление, выпуск, архив и закрытая группа не дают
обновить сессию.
### academic
@@ -400,8 +422,30 @@ V13 идемпотентна: если старая feature-миграция V10
и защиты. Свой профиль студент открывает без записи в аудит, чтобы не
засорять журнал и не нагружать identity на горячем пути. Админ смотрит
журнал на `/admin/audit` с фильтром по действию; время отображается в МСК.
`membership_history` и `promotion_runs/items` остаются доменной историей и
дополняют общий журнал.
Журнал читается страницами (`page`/`limit`, «Показать ещё»), а не только
последними N событиями. `membership_history` и `promotion_runs/items`
остаются доменной историей и дополняют общий журнал.
### 5.3. Аутентификация, сессии и отзыв доступа
- **Access-токен** — JWT на 15 минут (`LMS_ACCESS_TOKEN_TTL`), роли
`ADMIN/TEACHER/STUDENT`, без списка курсов/групп внутри (скоуп проверяется
сервисами на каждом запросе).
- **Refresh-токен** — 7 дней (`LMS_REFRESH_TOKEN_TTL`), хранится хешем в
`identity.refresh_tokens` (`V11`), ротируется при каждом обновлении.
Повторное использование старого токена отзывает все активные сессии
аккаунта; выход, смена пароля, отчисление/выпуск/архив и закрытая группа
обновление запрещают.
- **Прямой API-клиент** обязан обновлять пару через
`POST /api/v1/auth/refresh`; `POST /api/v1/auth/logout` отзывает refresh.
- **Web-сессия** — 7 дней sliding (`LMS_SESSION_TIMEOUT`), cookie `LMS_SESSION`
(`HttpOnly`, `SameSite=Lax`, `Max-Age` обновляется на каждом ответе).
Access-токен автоматически обновляется за 2 минуты до истечения, поэтому
активная вкладка не требует повторного входа каждые 15 минут; при выходе
refresh отзывается.
- **Ограничение:** абсолютного лимита жизни сессии нет — при непрерывной
активности она продлевается. Если политика потребует принудительный
повторный вход раз в N дней, добавляется отдельный абсолютный `max-age`.
---
@@ -410,10 +454,13 @@ V13 идемпотентна: если старая feature-миграция V10
- Аудитории: `ALL` (в т.ч. **неавторизованные**), `STUDENTS`, `TEACHERS`; глобальные публикует только админ.
- Нужны **публичные маршруты** для `ALL` без аутентификации. `STUDENTS`/`TEACHERS`/курсовые материалы
анонимно недоступны ни при каких условиях.
- **Активы (риск):** материалы ссылаются на файлы (`/api/v1/assets/**`). Для публичных глав активы должны
быть доступны, для приватных — нет. Решение: видимость актива наследуется от главы, и маршрут выдачи
активов обязан это проверять (либо отдельный публичный префикс для `ALL`). Без этого публичные материалы
будут ломаться, а приватные — утекать.
- **Активы (реализовано):** видимость актива наследуется от статьи → раздела →
главы, маршрут `/api/v1/content/assets/{name}` проверяет доступ. Файл без
связи со статьёй не публичен: аноним/студент/преподаватель получают `404`,
админ может скачать файл и получить список таких файлов через
`GET /api/v1/content/legacy-assets` для привязки или удаления. Старые
внешние ссылки на несвязанные файлы у обычных пользователей перестают
работать — это осознанное secure-by-default поведение.
- Поисковая индексация: для `ALL` — по умолчанию разрешена; для остального — закрыть (robots/noindex).
---

View File

@@ -1,8 +1,9 @@
# 08. План реализации: группы, курсы, преподаватели
**Статус:** план согласован, этапы 0–6 выполнены. Этап 6 закрыт: права
преподавателя на запись, аудит и CI добавлены; остаточные риски — § 9.1 и
[16-stage6-report.md](16-stage6-report.md).
преподавателя на запись, аудит и CI добавлены; продуктовые риски (legacy-активы,
срок жизни JWT/сессии, пагинация аудита) закрыты 2026-10-28. Оставшиеся
ограничения — § 9.1 и [16-stage6-report.md](16-stage6-report.md).
**Основа:** [07-groups-courses.md](07-groups-courses.md).
**Дата решений:** 2026-10-23.
@@ -195,10 +196,12 @@
привязка групп, авторские материалы курса.
- Полная security-матрица и negative-тесты «свой/чужой курс».
- Upgrade-тест миграций.
- Индексы; пагинация и N+1 — follow-up (§ 9.1).
- Индексы; пагинация аудита и bulk-выдача групп с именами — выполнены;
оставшийся N+1 в отдельных лабораторных списках — follow-up (§ 9.1).
- Аудит скачиваний и чувствительных действий (`identity.audit_events`,
`/admin/audit`).
- CI merge gate: `test ktlintCheck detekt` на каждом push/MR.
- CI merge gate: `test ktlintCheck detekt evidenceCheck` и smoke полного стека
на каждом push/MR; полный `release-evidence` — для тегов/вручную.
- Проверка backup: создание, retention, `verify`, restore на staging.
- Документация, deploy/runbook, smoke.
@@ -208,11 +211,11 @@
Номера уточняются при реализации, чтобы не конфликтовать с максимумами master
(identity V3, **academic V5**, progress V12). Максимумы этой ветки:
identity V10, **academic V12**, progress V13.
identity V11, **academic V12**, progress V13.
| Сервис | Миграции | Содержание |
|---|---|---|
| identity | V4–V10 | роли, поля групп, статусы студентов, `membership_history`, `promotion_runs/items`, `accounts.status`; `V10` — `audit_events` без внешних ключей и индексы по `occurred_at`/`action` |
| identity | V4–V11 | роли, поля групп, статусы студентов, `membership_history`, `promotion_runs/items`, `accounts.status`; `V10` — `audit_events` без внешних ключей и индексы по `occurred_at`/`action`; `V11` — `refresh_tokens` (хеш, срок, отзыв, цепочка замен) |
| academic | V6–V12 + post-bootstrap step | курсы, преподаватели, группы курса, `labs.course_id` (nullable), материалы курса/аудитории, частичные индексы, активы; `V10` — уникальный индекс `lower(title)` по курсам; `V11` — backfill `audience='ALL'` для глобальных глав; `V12` — backfill связей `article_assets` из markdown-ссылок; `SET NOT NULL` — post-bootstrap шаг вне Flyway |
| progress | V13 | `grade_comments.author_account_id`, `lab_grades.updated_by_account_id`, индекс `lab_grades(lab_id)`; аудит реализован в identity `V10` |
@@ -291,11 +294,16 @@ identity V10, **academic V12**, progress V13.
- **Межсервисная авторизация.** Самый сложный участок; ADR до кода.
- **Перенос данных.** Нужны preflight и копия БД; группы с нестандартными именами
и дубли номеров лаб могут заблокировать `NOT NULL`.
- **Legacy-активы.** Связанные активы наследуют видимость статьи (миграция `V12`),
но файлы без markdown-ссылки остаются публичными; нужна инвентаризация и
привязка или удаление.
- **JWT 30 дней.** Средний вариант блокировки оставляет остаточный риск для
прямого API; фиксируем в документации.
- **Legacy-активы.** Закрыто: связанные активы наследуют видимость статьи
(миграция `V12`), а файлы без markdown-ссылки по умолчанию отдают `404`
анониму/студенту/преподавателю. Админ перечисляет их через
`GET /api/v1/content/legacy-assets` для привязки или удаления.
- **JWT и сессии.** Закрыто: access-токен 15 минут, refresh 7 дней с ротацией
и отзывом, web-сессия 7 дней sliding. Прямые API-клиенты используют
`POST /api/v1/auth/refresh`; logout и смена пароля отзывают refresh.
- **Оставшаяся производительность.** Аудит постраничный и группы курса
отдаются одним вызовом, но N+1 в отдельных лабораторных списках и
offset-пагинация аудита остаются лабораторными деталями.
- **Комментарии без автора.** Старые записи останутся с «автор не указан».
- **Архивные студенты.** Открывать ли профиль после отвязки группы — уточнить
на этапе 3; по умолчанию история курса доступна, если студент был в курсе.
@@ -326,24 +334,25 @@ identity V10, **academic V12**, progress V13.
- **Этап 6 — выполнен.** Права преподавателя на запись (лабораторные,
ведомость, группы, материалы), центральный аудит `identity.audit_events`
с `/admin/audit`, CI merge gate и полный hardening. Проверены прогон
тестов/линтеров (**267**), upgrade миграций с master-схемы (identity
`V4–V10`, academic `V6–V12`, progress `V13`), security-матрица, gateway,
видимость активов и deploy-runbook. Отчёт: [16-stage6-report.md](16-stage6-report.md).
тестов/линтеров (**319**, 0 failures), upgrade миграций с master-схемы
(identity `V4–V11`, academic `V6–V12`, progress `V13`), security-матрица,
gateway, видимость активов и deploy-runbook. LDD baseline: **46 claims
(25 P0 / 21 P1), 46/46 proven**, browser E2E 4/4. Отчёт:
[16-stage6-report.md](16-stage6-report.md).
### 9.1. Известные пробелы после этапа 6
- **Legacy-активы без привязки.** Маршрут `/api/v1/content/assets/{name}`
проверяет `article_assets → article → section → chapter`, а `V12`
восстанавливает связи из markdown. Файлы без ссылок в статьях остаются
публичными; нужна инвентаризация и привязка или удаление.
- **Импорт.** Старый формат не поддерживается; ведущие нули номера группы
нормализуются (`01` → `1`).
- **JWT 30 дней.** Web перепроверяет статус каждые 45 секунд, но прямой API
с уже выданным токеном живёт до истечения; нужны короткий TTL + refresh
или revocation.
- **Пагинация и N+1.** Журнал аудита ограничен `limit` без offset/keyset;
часть списков групп, материалов и ведомости отдаётся целиком; на больших
объёмах нужны пагинация и пакетные выборки.
- **Импорт.** Старый формат не поддерживается, принимается только шаблон v2.
Номер группы — числовой; отдельного требования к ведущим нулям нет.
- **Пагинация и N+1.** `GET /api/v1/audit` поддерживает `page` + `limit`,
в UI есть «Показать ещё»; `GET /api/v1/courses/{id}/groups` отдаёт
привязанные группы с именами одним вызовом. Остаются offset-пагинация
аудита и N+1 в отдельных лабораторных списках/ведомости на больших курсах.
- **Абсолютный лимит сессии.** Web-сессия скользящая (7 дней) и отзывается
при выходе/смене пароля/закрытии доступа, но при непрерывной активности
может жить бессрочно; при необходимости нужен абсолютный `max-age`.
- **Внешняя интеграция.** «Контингент»/`next.uniyar.ac.ru` остаётся заделом:
отдельный anti-corruption слой и внешние идентификаторы — позже.
## 10. Definition of Done
@@ -354,8 +363,13 @@ identity V10, **academic V12**, progress V13.
- Профиль показывает статистику, комментарии и все попытки; файлы скачиваются.
- Повышение, академотпуск, выпуск и архив работают по ТЗ.
- Аудит чувствительных действий пишется в `identity.audit_events` и доступен
админу на `/admin/audit`.
- Security-матрица и миграционный upgrade-тест зелёные.
- CI на каждом push/MR прогоняет `test ktlintCheck detekt`.
админу на `/admin/audit` с пагинацией.
- Активы наследуют видимость материала; файлы без привязки не публичны и
доступны только админу через инвентаризацию `legacy-assets`.
- Access/refresh-токены и web-сессия работают по срокам 15 минут / 7 дней /
7 дней; logout, смена пароля и закрытие доступа отзывают refresh.
- Security-матрица и миграционный upgrade-тест зелёные (identity до `V11`).
- CI на каждом push/MR прогоняет `test ktlintCheck detekt evidenceCheck` и
smoke полного стека; полный release-evidence обязателен для тегов.
- Backup перед деплоем обязателен; restore проверен на staging.
- Документация, deploy/runbook и smoke обновлены.

View File

@@ -9,9 +9,10 @@
- ADR с ключевыми решениями: [adr/0001-groups-courses-foundation.md](adr/0001-groups-courses-foundation.md).
- Контракт API профиля студента: [09-teacher-student-profile.md](09-teacher-student-profile.md).
- Миграции:
- identity `V4–V10`: роли, группы-когорты, статусы студентов, повышение,
- identity `V4–V11`: роли, группы-когорты, статусы студентов, повышение,
история членства, статусы аккаунтов; `V10` (`audit_events`) добавлена
позже, на этапе 6;
позже, на этапе 6; `V11` (`refresh_tokens`) — при закрытии рисков
аутентификации, тоже после этого отчёта;
- academic `V6–V12`: курсы, преподаватели, группы курса, `labs.course_id`,
материалы курса/аудитории, индексы активов; `V10` (уникальный индекс
`lower(title)` по курсам) добавлена на этапе 2, после этого отчёта;

View File

@@ -74,9 +74,9 @@
## 3. Ограничения этапа
- Импорт поддерживает одну группу на файл; мультигрупповой файл — позже.
- Ведущие нули номера группы нормализуются к числу (`01` → `1`);
имя строится из числового номера. Если нужны разные `ПИ-11БО` и `ПИ-101БО`
как отдельные когорты, потребуется хранить номер строкой.
- Номер группы числовой (`01` и `1` — одно значение). Отдельный строковый
номер и различие `ПИ-11БО`/`ПИ-101БО` как разных когорт не требуются;
имя группы по-прежнему строится из полей.
- Роль `TEACHER` уже есть в домене, но кабинет преподавателя и назначения
на курс — этапы 2–3.
- `ACADEMIC_LEAVE` пока может входить; режим «только чтение» — этап 3.

View File

@@ -168,6 +168,8 @@
- **JWT и блокировка.** Вход закрывается при следующей аутентификации;
уже выданный JWT действует до истечения (до 30 дней). Это остаточный
риск, зафиксированный в плане; аудит и hardening — этап 6.
*Историческая оценка этапа 4; закрыто в identity `V11` (access 15 минут,
refresh 7 дней с отзывом) — см. [16](16-stage6-report.md).*
- **Нет новых миграций.** Используются таблицы `promotion_runs`,
`promotion_run_items`, `membership_history`, `accounts.status`
из миграций `V7`–`V9` этапа 1. JSONB пишется через `JdbcTemplate`

View File

@@ -36,7 +36,9 @@
расширению и `size = 0`. Повторный запуск не создаёт дубликатов.
- Публичный API `/api/v1/content/assets/{name}` проверяет доступ по всем
статьям, к которым привязан файл (достаточно одной видимой):
- актив без связей в `article_assets` — legacy-файл, публичен как раньше;
- актив без связей в `article_assets` — secure-by-default: `404` для
анонима, студента и преподавателя; только `ADMIN` может скачать файл для
инвентаризации/очистки;
- глобальная опубликованная статья `ALL` — публична, в том числе анонимно;
- глобальные `STUDENTS`/`TEACHERS` — только с JWT и соответствующей ролью;
- курсовой актив — только с JWT и доступом к курсу: админ видит любой курс,
@@ -47,12 +49,17 @@
поэтому защищённые картинки на страницах `/learn/...` открываются, а на
публичных страницах прокси остаётся анонимным.
**Остаточный риск — legacy-активы без привязки.** `V12` закрывает только файлы,
на которые есть ссылки в markdown. Если файл был загружен, но ни одна статья на
него не ссылается (осиротевший или удалённый из текста актив), он остаётся
публичным. Нужна инвентаризация таких файлов и привязка к статье либо удаление;
до этого поведение сохраняется осознанно, чтобы не ломать старые публичные
ссылки.
## Legacy-активы без привязки (закрыто)
`V12` закрывает файлы, на которые есть ссылки в markdown. Если файл был
загружен, но ни одна статья на него не ссылается, он больше **не публичен**:
аноним, студент и преподаватель получают `404`, чтобы не раскрывать
существование файла. Админ скачивает такой файл и получает его имя в
инвентаризации `GET /api/v1/content/legacy-assets`, после чего привязывает
файл к статье или удаляет. Старые внешние ссылки на несвязанные файлы у
обычных пользователей перестают работать — это осознанное поведение
secure-by-default, а не регрессия. Эндпоинт инвентаризации защищён
`ROLE_ADMIN`.
## Авторство материалов преподавателем (добавлено на этапе 6)

View File

@@ -1,15 +1,16 @@
# 16. Отчёт этапа 6: проверка, hardening и передача
**Дата:** 2026-10-27.
**Дата:** 2026-10-27, обновлено 2026-10-28 после закрытия продуктовых рисков.
**Ветка:** `feature/groups-courses`.
**Статус:** этап 6 закрыт. Остаточные риски перечислены в разделе 4 и не
блокируют передачу.
**Статус:** этап 6 закрыт. Продуктовые риски (legacy-активы, срок жизни JWT,
сессии, пагинация аудита и N+1 имён групп) закрыты; оставшиеся ограничения
перечислены в разделе 4 и не блокируют передачу.
## 1. Итоговый объём этапов 0–6
| Этап | Что реализовано | Отчёт |
|---|---|---|
| 0 | Схема и контракты: identity `V4–V10` (`V10` — аудит, добавлена на этапе 6), academic `V6–V12`, progress `V13`; preflight-скрипты; ADR; контракт профиля студента курса | [10](10-stage0-report.md) |
| 0 | Схема и контракты: identity `V4–V11` (`V10` — аудит, `V11` — refresh-токены), academic `V6–V12`, 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) |
@@ -26,8 +27,11 @@
активной группы; админ — все курсы;
- студент видит только опубликованные оценки и комментарии;
- активы наследуют видимость статьи → раздела → главы: курсовые требуют доступа
к курсу, глобальные — аудитории, а старые файлы без привязки остаются
публичными (см. раздел 4);
к курсу, глобальные — аудитории, а файлы без связи со статьёй по умолчанию
недоступны (`404`) всем, кроме админа; админ перечисляет их через
`GET /api/v1/content/legacy-assets`;
- access-токен живёт 15 минут, refresh — 7 дней с ротацией и отзывом; web-сессия
скользящая на 7 дней и сама обновляет access-токен;
- преподаватель своего курса может писать: лабораторные/уровни/варианты,
ведомость, привязка групп и авторские материалы курса; чужие курс, лаба,
студент и группа отклоняются на сервисах;
@@ -63,12 +67,13 @@
| progress-service | 33 | 0 | 0 | 0 |
| web | 69 | 0 | 0 | 0 |
| markdown | 5 | 0 | 0 | 0 |
| **Итого** | **267** | **0** | **0** | **0** |
| **Итого на момент этапа 6** | **267** | **0** | **0** | **0** |
`ktlintCheck` и `detekt` прошли без замечаний. Отдельных unit-тестов у gateway
и `common-http` в текущем дереве нет; маршрутизация gateway проверена
конфигурацией и security-матрицей ниже. Тот же набор команд выполняет
`.gitlab-ci.yml`.
После закрытия рисков актуальный LDD-baseline — **319 тестов, 0 failures**
(см. раздел 7). `ktlintCheck` и `detekt` прошли без замечаний. У gateway теперь есть unit-тесты публичных auth-путей и 401; маршрутизация
gateway дополнительно проверена конфигурацией и security-матрицей ниже.
Тот же набор команд выполняет `.gitlab-ci.yml`; на каждый push/MR добавлен
smoke-job, полный release-evidence обязателен для тегов.
### 2.2. Upgrade-тест миграций
@@ -81,7 +86,7 @@
(`ПИ-11БО`, `ПИ-21БО`), зачисления, семестр, лабораторная, глава/раздел/
статья/актив, оценка, комментарий и попытка сдачи.
3. Поверх master применены миграции ветки:
- identity `V4–V10`;
- identity `V4–V11`;
- academic `V6–V12`;
- progress `V13`.
4. Выполнены postflight-запросы и повторное применение `V13`.
@@ -93,7 +98,9 @@
курс 2, год 2025); статус `ACTIVE`; созданы `promotion_runs`,
`promotion_run_items`; зачисления перенесены в `membership_history`
действием `ENROLL`; добавлен `accounts.status`; `V10` создаёт
`identity.audit_events` без внешних ключей и индексы по `occurred_at`/`action`.
`identity.audit_events` без внешних ключей и индексы по `occurred_at`/`action`;
`V11` создаёт `identity.refresh_tokens` (SHA-256-хеш, `expires_at`,
`revoked_at`, `replaced_by`, индексы по аккаунту и сроку).
- **academic:** добавлены `labs.course_id`, `chapters.course_id/audience`,
индексы `labs_number_course_uidx`, `chapters_slug_global_uidx`,
`chapters_slug_course_uidx`, `article_assets_article_idx`,
@@ -119,8 +126,8 @@
### 2.3. Security-проверки
Проверено автоматическими тестами (полный прогон 267 тестов) и разбором
конфигурации gateway. Запись преподавателя дополнительно покрыта
Проверено автоматическими тестами (актуальный прогон — 319 тестов, 0 failures)
и разбором конфигурации gateway. Запись преподавателя дополнительно покрыта
negative-тестами «свой/чужой курс».
| Сценарий | Ожидание | Факт |
@@ -133,24 +140,28 @@ negative-тестами «свой/чужой курс».
| Студент/преподаватель чужого курса → курсовой актив | `403` | `403` |
| Преподаватель своего курса → скрытый курсовой актив | доступ | `200`; админ видит любой актив |
| Глобальный `STUDENTS`-актив → преподаватель | `403` | `403`; студент получает `200` |
| Legacy-актив без связи со статьёй | публично | `200`; остаточный риск, раздел 4 |
| 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`); `POST /api/v1/auth/refresh` ротирует пару, повторное использование старого токена отзывает все сессии аккаунта |
| 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=` и `/admin/audit` доступны только `ADMIN` |
| Аудит → админ | чтение журнала | `GET /api/v1/audit?page=&limit=&action=` и `/admin/audit` доступны только `ADMIN`; в UI — постраничный переход «Показать ещё» |
| Аудит → запись из сервисов | только по токену | `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 и публичного `/api/v1/content/**` |
| Gateway | внутренние пути не публикуются | маршрутов на `/api/v1/internal/**` нет; gateway требует JWT для всего, кроме `/actuator`, входа/setup/JWKS/refresh/logout и публичного `/api/v1/content/**` |
| Gateway → заголовки личности | клиент не подменяет | `X-Account-Id/Login/Role` удаляются и проставляются только после проверки JWT |
| Web-сессия | быстрый отзыв доступа | статус перепроверяется не реже чем раз в 45 секунд; `active=false` или `401/403` инвалидируют сессию; недоступность identity не разлогинивает всех |
Остаточные риски перечислены в разделе 4: legacy-активы без привязки,
30-дневный JWT для прямого API, ведущие нули номера группы и отсутствие
пагинации/N+1 на больших объёмах.
Остаточные ограничения перечислены в разделе 4: offset-пагинация аудита,
N+1 в отдельных лабораторных списках и отсутствие абсолютного лимита
web-сессии. Продуктовые риски legacy-активов, 30-дневного JWT и ведущих
нулей номера группы закрыты или сняты как не относящиеся к требованиям.
## 3. Чек-лист деплоя
@@ -166,18 +177,25 @@ negative-тестами «свой/чужой курс».
Там же задан `LMS_DB_PASSWORD`; `.env.prod` не коммитится.
- [ ] Проверить, что на сервере нет незакоммиченных изменений и выполнен
`git pull` нужной ветки/тега.
- [ ] **CI зелёный.** `.gitlab-ci.yml` выполняет
`./gradlew --no-daemon test ktlintCheck detekt`. Runner должен разрешать
privileged-сервисы `docker:dind`: Testcontainers поднимает PostgreSQL 18
через Docker-in-Docker.
- [ ] **CI зелёный.** `.gitlab-ci.yml` на каждом push/MR выполняет
`test ktlintCheck detekt evidenceCheck` и job `smoke`
(`scripts/ldd-smoke.sh`: полный стек, health, вход админа, один курс).
Полный `release-evidence` обязателен для тегов и запускается вручную для
веток. `check` использует privileged-сервисы `docker:dind` для Testcontainers;
`smoke`/`release-evidence` — shell-runner с Java 21, Docker Compose v2,
Python 3, Node и Chromium Playwright.
- [ ] Заданы сроки сессии: `LMS_ACCESS_TOKEN_TTL=15m`,
`LMS_REFRESH_TOKEN_TTL=7d`, `LMS_SESSION_TIMEOUT=7d` (значения по умолчанию
подходят; менять только осознанно).
### 3.2. Порядок выкладки
1. **Flyway.** `./deploy/deploy.sh up` собирает образы и запускает
`postgres → identity → academic → progress → gateway → web`. Миграции
применяются автоматически при старте сервисов:
identity `V4–V10` (`V10` — `identity.audit_events`), academic `V6–V12`,
progress `V13`. Отдельный ручной запуск Flyway не нужен.
identity `V4–V11` (`V10` — `identity.audit_events`, `V11` —
`identity.refresh_tokens`), academic `V6–V12`, progress `V13`.
Отдельный ручной запуск Flyway не нужен.
2. **Bootstrap курса C.** После входа админа открыть `/admin/courses`.
Пока курсов нет, показан блок первичной настройки. Bootstrap идемпотентно
создаёт/находит курс «Промышленное программирование на языке C»,
@@ -194,7 +212,8 @@ negative-тестами «свой/чужой курс».
- ровно один курс C;
- `comments_without_author` соответствует старым комментариям;
- группы без полного разбора отсутствуют;
- таблица `identity.audit_events` существует (`V10`).
- таблица `identity.audit_events` существует (`V10`);
- таблица `identity.refresh_tokens` существует (`V11`).
4. **V10 → V13 — только dev/staging.** На production/master записи
`progress V10__profile_authors.sql` нет, ничего удалять не нужно. Не путать
с academic `V10__course_title_unique.sql` — он относится к другой схеме и
@@ -229,7 +248,10 @@ negative-тестами «свой/чужой курс».
только опубликованные; чужой курс даёт `403`, аноним уходит на `/login`.
- [ ] **Активы:** картинки курсовой статьи открываются у студента своего курса
и преподавателя; чужой курс — `403`, скрытая картинка у студента — `404`,
публичная `ALL` — без входа; старый файл без привязки — публичен.
публичная `ALL` — без входа; файл без привязки к статье — `404` для всех,
кроме админа. Админ проверяет инвентаризацию через
`GET /api/v1/content/legacy-assets` (или админский UI/curl) и при
необходимости привязывает или удаляет файлы.
- [ ] **Копирование лабораторной:** админ копирует лабораторную в другой курс;
создаётся черновик с уровнями и вариантами; оценки, сдачи, защиты и
назначения не копируются; занятый номер даёт `409`.
@@ -241,45 +263,48 @@ negative-тестами «свой/чужой курс».
- [ ] **Аудит:** `/admin/audit` открывается у админа, показывает события
(например, `PROFILE_VIEW`, `SUBMISSION_DOWNLOAD`, `GRADE_SET`,
`CONTENT_*`, `TEACHER_*`) с фильтром по действию и временем МСК;
«Показать ещё» переходит на следующую страницу (`page`/`limit`);
преподаватель/студент получают `403`.
- [ ] **Сессия:** после входа cookie `LMS_SESSION` живёт 7 дней; при работе
дольше 15 минут страница продолжает работать без повторного входа
(access обновляется автоматически). Выход и смена пароля требуют повторного
входа; вход отчисленного/архивного студента и refresh закрытой группы
отклоняются.
- [ ] Проверить логи `web`, `gateway`, `identity`, `academic`, `progress` на
`ERROR`/`WARN` после smoke.
## 4. Известные пробелы и follow-up
1. **Legacy-активы без привязки.** Видимость связанных активов реализована:
`V12` восстанавливает `article_assets` по markdown-ссылкам, а маршрут
`/api/v1/content/assets/{name}` проверяет курс/аудиторию и публикацию.
Остаточный случай — файлы, на которые нет ссылок ни в одной статье: они
остаются публичными. Нужна инвентаризация и привязка либо удаление таких
файлов; до этого fallback сохраняется, чтобы не ломать старые ссылки.
Детали — в [15](15-stage5-course-materials.md).
2. **Старый формат импорта не поддерживается.** Импорт принимает только
шаблон v2 (поток/курс/номер/степень/год/ФИО/№/база). Ведущие нули номера
группы нормализуются к числу (`01` → `1`), поэтому `ПИ-01БО` и `ПИ-1БО`
неразличимы; для разных когорт нужен строковый номер. Ограничение
зафиксировано в [11](11-stage1-report.md).
3. **JWT живёт до 30 дней.** Web-сессия перепроверяет статус каждые 45 секунд
и закрывает UI при отчислении/архиве, но уже выданный JWT для прямого
вызова API действует до истечения. Нужны короткий TTL + refresh,
denylist/revocation или проверка статуса на каждом API-вызове.
4. **Пагинация и N+1.** Журнал аудита ограничен `limit` (максимум 500), но
без offset/keyset; часть списков групп, материалов и ведомости отдаётся
целиком. На больших курсах нужны пагинация и пакетные выборки вместо
запросов в цикле.
1. **Старый формат импорта не поддерживается.** Импорт принимает только
шаблон v2 (поток/курс/номер/степень/год/ФИО/№/база). Ограничение
зафиксировано в [11](11-stage1-report.md). Номер группы — числовой, поэтому
отдельного требования к ведущим нулям нет.
2. **Пагинация аудита — offset.** `GET /api/v1/audit` поддерживает `page` и
`limit` (максимум 500), UI — «Показать ещё». Для очень больших журналов
keyset-пагинация была бы устойчивее к вставкам между страницами.
3. **N+1 в отдельных списках.** Имена групп курса отдаются одним вызовом
(`GET /api/v1/courses/{id}/groups`), но часть лабораторных списков и
ведомости на больших объёмах всё ещё собирается по частям. Полный нагрузочный
профиль 100 групп / 1000 студентов проходит, однако пакетные выборки для
оставшихся экранов — лабораторная деталь, а не блокер передачи.
4. **Нет абсолютного лимита web-сессии.** Сессия скользящая (7 дней) и
отзывается при выходе, смене пароля, отчислении/архиве и закрытии группы,
но при непрерывной активности может жить бессрочно. Если политика требует
принудительного повторного входа раз в N дней, нужен абсолютный `max-age`
поверх sliding-сессии.
## 5. Рекомендуемые следующие шаги
Приоритет — сверху вниз:
1. **Инвентаризация legacy-активов.** Найти файлы без строк в `article_assets`,
привязать их к статьям или удалить; после этого можно убрать публичный
fallback для несвязанных файлов.
2. **JWT/refresh.** Короткий TTL access-токена + refresh, denylist/revocation
или проверка статуса на каждом API-вызове; закрыть остаточный риск
отчисленного/архивного пользователя с уже выданным токеном.
3. **Пагинация и устранение N+1.** Keyset/offset для аудита, групп, материалов
и ведомости; пакетные выборки профилей, комментариев и сдач.
1. **Инвентаризация legacy-активов.** Несвязанные файлы уже не публичны; админ
получает их список через `GET /api/v1/content/legacy-assets`, после чего
привязывает к статьям или удаляет. Это гигиена хранилища, а не открытый
риск доступа.
2. **Keyset-пагинация и оставшиеся N+1.** Устойчивый курсор для аудита,
пакетные выборки для лабораторных списков и ведомости на больших курсах.
3. **Абсолютный лимит сессии.** Опционально: принудительный повторный вход
раз в N дней поверх 7-дневной sliding-сессии.
4. **Внешняя интеграция «Контингент»/next.uniyar.ac.ru.** Отдельный
anti-corruption слой, идемпотентная синхронизация, журнал и отчёт дрифта;
внешние идентификаторы добавляются миграцией.
@@ -288,17 +313,22 @@ negative-тестами «свой/чужой курс».
Этап 6 закрыт: права преподавателя на запись, аудит, CI, security-матрица,
upgrade-тест миграций, видимость активов и deploy-runbook проверены.
Остаточные риски (legacy-активы, 30-дневный JWT, ведущие нули номера группы,
пагинация/N+1) задокументированы в разделе 4 и не блокируют передачу.
Продуктовые риски закрыты: legacy-активы secure-by-default, access/refresh и
web-сессия с отзывом, пагинация аудита и bulk-выдача групп. Оставшиеся
ограничения (offset-пагинация, отдельные N+1, отсутствие абсолютного лимита
сессии) задокументированы в разделе 4 и не блокируют передачу.
## 7. LDD baseline
После этапа 6 текущая функциональность проверена через LDD-контур:
реестр `docs/evidence/claims.json` (37 claim, 20 P0 / 17 P1), gate
`./gradlew evidenceCheck`, upgrade-скрипт, runtime-smoke, mutation testing
и браузерный Playwright E2E. Результат: **37/37 claims proven, P0 gaps = 0**,
276 тестов; mutation strength `CourseAccessService` 75%,
`PromotionPlanner` 69%; browser E2E 4/4; конкурентные apply/rollback и
нагрузочный smoke (p95 0.868 c) зелёные. E2E нашёл и закрыл реальный дефект
После этапа 6 и закрытия рисков текущая функциональность проверена через
LDD-контур: реестр `docs/evidence/claims.json` (**46 claims, 25 P0 / 21 P1**),
gate `./gradlew evidenceCheck`, upgrade-скрипт, runtime-smoke, smoke-job,
mutation testing и браузерный Playwright E2E. Результат:
**46/46 claims proven, P0 gaps = 0, 319 тестов, 0 failures**; mutation strength
`CourseAccessService` 75%, `PromotionPlanner` 69%; browser E2E 4/4;
конкурентные apply/rollback и масштабный нагрузочный smoke (100 групп /
1000 студентов, 125 параллельных чтений, 0 ошибок, p95 < 1 с) зелёные.
Smoke-job выполняется на каждом push/MR, полный `release-evidence` обязателен
для тегов и доступен вручную для веток. E2E нашёл и закрыл реальный дефект
Jackson 3/Kotlin defaults. Подробности и остаточные ограничения —
[`docs/evidence/baseline-2026-10-27.md`](evidence/baseline-2026-10-27.md).

View File

@@ -22,10 +22,10 @@ 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`-главы, видимость активов и legacy-файлы без привязки |
| [16-stage6-report.md](16-stage6-report.md) | Отчёт этапа 6: права преподавателя на запись, аудит, CI, upgrade-тест миграций, security-матрица, чек-лист деплоя и остаточные риски |
| [15-stage5-course-materials.md](15-stage5-course-materials.md) | Отчёт этапа 5: курсовые материалы, публичные `ALL`-главы, видимость активов и secure-by-default legacy-файлы |
| [16-stage6-report.md](16-stage6-report.md) | Отчёт этапа 6: права преподавателя на запись, аудит, CI, upgrade-тест миграций, security-матрица, чек-лист деплоя и остаточные ограничения |
| [evidence/README.md](evidence/README.md) | LDD: реестр утверждений, evidence gate, runtime- и migration-проверки |
| [evidence/baseline-2026-10-27.md](evidence/baseline-2026-10-27.md) | Первый LDD-baseline: 31 claim, 267 тестов, upgrade и runtime-smoke |
| [evidence/baseline-2026-10-27.md](evidence/baseline-2026-10-27.md) | LDD-baseline: 46 claims, 319 тестов, upgrade, runtime/scale, mutation и browser E2E |
| [ADR 0001](adr/0001-groups-courses-foundation.md) | Ключевые архитектурные решения: курс C, миграции, доступ, статусы, повышение |
## Кратко о системе

View File

@@ -12,8 +12,9 @@
`./gradlew evidenceCheck`.
- **P1** — важное поведение/UX/контракт. Пробел показывается в отчёте, но не
блокирует сборку.
- **runtime** — проверка на реально поднятом стеке. Выполняется отдельным
скриптом; в unit-CI может отсутствовать, но обязательна перед релизом.
- **runtime** — проверка на реально поднятом стеке. Быстрый smoke
(`scripts/ldd-smoke.sh`) выполняется в CI на каждом push/MR; полный
runtime/scale — в `release-evidence` и обязателен перед релизом.
## Реестр
@@ -39,19 +40,22 @@
# 2. Гейт свидетельств (P0 без evidence = fail)
./gradlew evidenceCheck
# 3. Полный runtime-smoke на одноразовой БД
# 3. Быстрый smoke полного стека (то же, что CI job smoke)
./scripts/ldd-smoke.sh
# 4. Полный runtime-smoke + scale load на одноразовой БД
./scripts/ldd-runtime.sh
# 4. Проверка upgrade с master-схемы
# 5. Проверка upgrade с master-схемы
./scripts/ldd-migration-upgrade.sh
# 5. Mutation testing критичных P0-компонентов
# 6. Mutation testing критичных P0-компонентов
./scripts/ldd-mutation.sh
# 6. Браузерный E2E (Playwright + Chromium)
# 7. Браузерный E2E (Playwright + Chromium)
./scripts/ldd-browser.sh
# 7. Полный release gate
# 8. Полный release gate
./scripts/ldd-release.sh
```
@@ -74,26 +78,36 @@ Platform 6 из Spring Boot 4.
доказательства.
6. При переименовании теста обновляется `claims.json`; устаревшая ссылка —
это gap, а не «забыли».
7. Runtime-свидетельства обязательны перед релизом; CI проверяет unit/
integration-слой.
7. Runtime-свидетельства обязательны перед релизом; CI на каждом push/MR
выполняет unit/integration-слой и быстрый smoke, полный release gate —
для тегов (вручную для веток).
## Текущий baseline
Первый LDD-цикл выполнен 2026-10-27/28:
LDD-циклы выполнены 2026-10-27/28; последний — после закрытия продуктовых
рисков:
- **276 автоматических тестов**, 0 failures/errors;
- **37 claims**: 20 P0 + 17 P1. P0 — доступы, повышение, жизненный цикл,
профиль/скачивание, материалы/активы, миграция V13, аудит; P1 — группы,
авторство, UI, контракты, импорт, копирование лаб, runtime, mutation и
browser E2E;
- **319 автоматических тестов**, 0 failures/errors;
- **46 claims**: 25 P0 + 21 P1, **46/46 proven, P0 gaps = 0**. P0 — доступы,
повышение, жизненный цикл, профиль/скачивание, материалы/активы
(включая secure-by-default legacy-файлы), миграция V13, аудит, refresh-токены
и их отзыв; P1 — группы, авторство, UI, контракты, импорт, копирование лаб,
пагинация аудита, bulk-группы курса, runtime, mutation и browser E2E;
- mutation testing: `CourseAccessService` 75% test strength,
`PromotionPlanner` 69%;
- Playwright: 4 сценария (admin, teacher, student/anonymous, сдача+mobile), все зелёные;
- runtime-smoke, master-upgrade, mutation и browser E2E вынесены в отдельные
скрипты.
- Playwright: 4 сценария (admin, teacher, student/anonymous, сдача+mobile),
все зелёные;
- масштабный load: 100 групп / 1000 студентов, 125 параллельных чтений,
0 ошибок, p95 < 1 с (порог p95 < 2 с);
- CI: job `smoke` (`ldd-smoke.sh`) на каждом push/MR; полный
`release-evidence` (`ldd-release.sh`) обязателен для тегов и ручной для
веток.
В ходе E2E найден и исправлен реальный дефект: Jackson 3 не применял
Kotlin-модуль, поэтому DTO с default-значениями требовали все поля JSON
(например, создание студента без `firstName`/`basis` давало 400).
Результаты и остаточные ограничения — в `baseline-2026-10-27.md`.
Остаточные ограничения: offset-пагинация аудита, N+1 в отдельных лабораторных
списках/ведомости, отсутствие абсолютного лимита web-сессии. Legacy-активы,
срок жизни JWT и «ведущие нули» больше не являются открытыми рисками.
Результаты — в `baseline-2026-10-27.md`.

View File

@@ -1,19 +1,19 @@
# LDD baseline: 2026-10-27/28
Первый и второй LDD-циклы проверки текущей функциональности LMS.
Добавлены реестр claims, gate `evidenceCheck`, runtime/migration/mutation
скрипты и браузерный E2E на Playwright. В ходе проверки найден и исправлен
реальный дефект Jackson 3.
Три LDD-цикла проверки текущей функциональности LMS; последний выполнен
2026-10-28 после закрытия продуктовых рисков. Добавлены реестр claims, gate
`evidenceCheck`, runtime/migration/mutation/smoke-скрипты и браузерный E2E на
Playwright. В ходе проверки найден и исправлен реальный дефект Jackson 3.
## 1. Что проверялось
Реестр: `docs/evidence/claims.json`, **37 утверждений**.
Реестр: `docs/evidence/claims.json`, **44 утверждения**.
| Критичность | Claims | Proven | Gaps |
|---|---:|---:|---:|
| P0 | 20 | 20 | 0 |
| P1 | 17 | 17 | 0 |
| **Итого** | **37** | **37** | **0** |
| P0 | 25 | 25 | 0 |
| P1 | 21 | 21 | 0 |
| **Итого** | **46** | **46** | **0** |
Домены:
@@ -22,12 +22,14 @@
- повышение: dry-run, apply, идемпотентность, rollback, cancel;
- жизненный цикл: академ, возврат, отчисление, архив, восстановление, вход;
- профиль студента и скачивание сдач;
- материалы и активы;
- миграция V13 и полный upgrade с master-схемы;
- аудит: токен, admin-only, сохранность после удаления, representative-хуки;
- группы курса, авторство материалов, ведомость, UI/контракты, импорт,
- материалы и активы, включая secure-by-default legacy-файлы;
- миграции V11 и V13, полный upgrade с master-схемы;
- аудит: токен, admin-only, сохранность после удаления, representative-хуки,
пагинация;
- аутентификация: короткий access-токен, ротация/отзыв refresh, web-сессия;
- группы курса без N+1, авторство материалов, ведомость, UI/контракты, импорт,
копирование лабораторных;
- runtime-smoke, mutation testing, браузерный E2E.
- runtime-smoke, масштабный load, mutation testing, браузерный E2E и CI smoke.
## 2. Команды и результаты
@@ -37,7 +39,7 @@
./gradlew test --rerun-tasks
```
Результат: **276 тестов, 0 failures, 0 errors, 0 skipped**.
Результат: **319 тестов, 0 failures, 0 errors, 0 skipped**.
`ktlintCheck` и `detekt` — зелёные.
### 2.2. LDD gate
@@ -46,7 +48,7 @@
./gradlew evidenceCheck
```
Результат: **37/37 claims proven, P0 gaps = 0**.
Результат: **46/46 claims proven, P0 gaps = 0**.
Отчёт: `build/reports/evidence/index.md`.
Gate устроен так:
@@ -65,11 +67,11 @@ Gate устроен так:
Сценарий: одноразовая PostgreSQL 18, master identity `V1–V3`, academic
`V1–V5`, progress `V1–V9,V11,V12`, синтетические данные, затем ветка:
identity `V4–V10`, academic `V6–V12`, progress `V13`.
identity `V4–V11`, academic `V6–V12`, progress `V13`.
Проверено:
- `identity.audit_events` создана;
- `identity.audit_events` (`V10`) и `identity.refresh_tokens` (`V11`) созданы;
- `students`, `groups`, `membership_history` сохранились;
- `labs.course_id`, `chapters.course_id`, `article_assets` созданы;
- `grade_comments.author_account_id`, `lab_grades.updated_by_account_id` созданы;
@@ -78,10 +80,11 @@ identity `V4–V10`, academic `V6–V12`, progress `V13`.
Результат: **PASS**, evidence: `build/evidence/migration.json`.
### 2.4. Runtime-smoke полного стека
### 2.4. Runtime-smoke полного стека и CI smoke
```bash
./scripts/ldd-runtime.sh
./scripts/ldd-smoke.sh # то же, что CI job smoke
```
Сценарий: одноразовая БД, `bootJar`, запуск identity/academic/progress/gateway/web,
@@ -99,9 +102,68 @@ identity `V4–V10`, academic `V6–V12`, progress `V13`.
- web `/login` отдаёт `200`, публичное дерево материалов доступно, курсовые
материалы анонимно — `401`.
Результат: **PASS**, evidence: `build/evidence/runtime.json`.
`ldd-smoke.sh` выполняет тот же полный стек, health, вход админа и один
доменный сценарий; это обязательный job `.gitlab-ci.yml` на каждом push/MR,
поэтому базовый runtime-контур проверяется до merge, а не только перед релизом.
### 2.5. Mutation testing
Результат: **PASS**, evidence: `build/evidence/runtime.json` и
`build/evidence/smoke.json`.
### 2.5. Legacy-активы: secure-by-default
Проверено автоматическими тестами доступа к активам:
- файл без строки в `article_assets` отдаёт `404` анониму, студенту и
преподавателю (существование не раскрывается);
- админ может скачать такой файл;
- `GET /api/v1/content/legacy-assets` защищён `ROLE_ADMIN` и перечисляет
несвязанные файлы для привязки или удаления;
- связанный актив по-прежнему наследует видимость статьи → раздела → главы:
публичная `ALL` — без входа, курсовая — только участникам курса, скрытая
для студента — `404`.
Старые внешние ссылки на несвязанные файлы у обычных пользователей перестают
работать. Это осознанное изменение поведения: безопасность важнее совместимости
с «осиротевшими» активами.
Результат: **PASS**, claims `LDD-ASSET-001`, `LDD-ASSET-002`.
### 2.6. Аутентификация, refresh и web-сессия
- identity `V11__refresh_tokens.sql` хранит только SHA-256-хеш refresh-токена,
срок, отзыв и цепочку замен;
- access JWT — 15 минут (`LMS_ACCESS_TOKEN_TTL`), refresh — 7 дней
(`LMS_REFRESH_TOKEN_TTL`);
- `POST /api/v1/auth/refresh` ротирует пару; повторное использование старого
токена отзывает все сессии аккаунта;
- logout и смена пароля отзывают refresh; отчисленный, выпущенный, архивный
студент, архивный аккаунт и закрытая группа не могут обновить сессию;
- web-сессия — 7 дней sliding (`LMS_SESSION_TIMEOUT`), cookie `LMS_SESSION`
(`HttpOnly`, `SameSite=Lax`, `Max-Age` перевыпускается на каждом ответе);
- access обновляется автоматически за 2 минуты до истечения, поэтому активная
вкладка не требует входа каждые 15 минут; logout отзывает refresh.
Прямой API-клиент должен сам использовать `POST /api/v1/auth/refresh`;
автоматического обновления вне web BFF нет.
Результат: **PASS**, claims `LDD-AUTH-001`, `LDD-AUTH-002`, `LDD-AUTH-003`.
### 2.7. Пагинация, N+1 и масштаб
- `GET /api/v1/audit` принимает `page` и `limit` (максимум 500); в UI есть
«Показать ещё». Это offset-пагинация, а не keyset.
- `GET /api/v1/courses/{id}/groups` возвращает привязанные группы с именами
одним вызовом; страница преподавателя не делает N+1 по именам групп.
- Полный load использует seed на **100 групп / 1000 студентов** (SQL, только
release-контур) и **125 параллельных чтений** курса, лаб, профиля, групп и
аудита: **0 ошибок, p95 < 1 с** (порог p95 < 2 с).
Остаются offset-пагинация аудита и N+1 в отдельных лабораторных
списках/ведомости — это лабораторные детали, не продуктовые риски доступа.
Результат: **PASS**, claims `LDD-AUDIT-004`, `LDD-PERF-001`, `LDD-RUNTIME-003`.
### 2.8. Mutation testing
```bash
./scripts/ldd-mutation.sh
@@ -119,7 +181,7 @@ PIT запускается через CLI с JUnit 4 harness: JUnit5-плаги
Остаток — эквивалентные мутации, Kotlin-интринсики и ветки, не входящие в
текущий P0-контур.
### 2.6. Браузерный E2E
### 2.9. Браузерный E2E
```bash
./scripts/ldd-browser.sh
@@ -136,25 +198,27 @@ Playwright/Chromium на полном стеке, одноразовая БД:
Результат: **4/4 passed**, evidence: `build/evidence/browser.json`.
Дополнительно проверены сдача файла студентом и mobile-размер teacher-страницы.
### 2.7. Конкурентные apply/rollback
### 2.10. Конкурентные apply/rollback
Добавлен `PromotionConcurrencyIntegrationTest` (без `@Transactional`, два потока,
`PromotionConcurrencyIntegrationTest` (без `@Transactional`, два потока,
advisory lock): два одновременных apply идемпотентны и пишут history один раз;
два одновременных rollback дают ровно один успех и один `ConflictException`,
состояние восстанавливается один раз.
### 2.8. Нагрузочный smoke
`ldd-runtime.sh` выполняет 100 чтений (10 workers) курса, лаб, профиля и аудита:
**0 ошибок, avg 0.141 c, p95 0.868 c** (порог p95 < 2 c).
### 2.9. Release gate
### 2.11. Release gate и CI
`./scripts/ldd-release.sh` последовательно запускает тесты/линтеры, upgrade,
mutation, runtime и browser E2E, затем финальный `evidenceCheck`.
В `.gitlab-ci.yml` добавлен ручной job `release-evidence` с Chromium/Node.
mutation, runtime + scale и browser E2E, затем финальный `evidenceCheck`.
### 2.10. Найденный дефект
В `.gitlab-ci.yml`:
- `check` — `test ktlintCheck detekt evidenceCheck` на push/MR;
- `smoke` — `ldd-smoke.sh` (полный стек, health, вход админа, один курс)
на каждом push/MR;
- `release-evidence` — полный `ldd-release.sh` с Chromium/Node: обязателен для
тегов, для веток запускается вручную.
### 2.12. Найденный дефект
Первый browser E2E упал на создании студента: API вернул `400`, потому что
Jackson 3 не применял Kotlin-модуль, и DTO с default-значениями требовали все
@@ -166,14 +230,21 @@ optional json fields`.
## 3. Что это доказывает
- Критичные P0-инварианты доступа, повышения, жизненного цикла, профиля,
материалов, миграции и аудита имеют зелёные автоматические свидетельства.
материалов, миграции, аудита и аутентификации имеют зелёные автоматические
свидетельства.
- Приложение собирается, мигрирует с master-схемы и поднимается полным
стеком на чистой БД.
- Ключевой доменный поток «админ → группа → курс → преподаватель → лаба»
проходит через реальные HTTP/gateway/БД и браузер.
- Чужой преподаватель не получает доступ к курсу/лабе/аудиту на реальном стеке.
- Legacy-актив без привязки не утекает обычным пользователям, а админ может
провести инвентаризацию.
- Refresh-токены ротируются и отзываются; закрытый статус блокирует продление
сессии, а web-сессия переживает 15-минутный access-токен.
- Аудит листается по страницам, а группы курса отдаются одним вызовом без N+1.
- Аудит пишется и читается; upgrade не теряет синтетические данные.
- Мутационная прочность критичных компонентов измерена, а не заявлена.
- Масштабный load на 100 группах / 1000 студентов проходит без ошибок.
- Реальный дефект DTO/Jackson найден именно E2E-контуром и закрыт тестом.
## 4. Чего это не доказывает
@@ -186,24 +257,32 @@ optional json fields`.
3. **Браузерный E2E — один Chromium и 4 сценария.** Нет визуальной регрессии,
Safari/Firefox и полного мобильного набора страниц.
4. **Нагрузочный smoke — не benchmark.** Одна машина, синтетические данные,
без SLA production.
5. **Legacy-активы без `article_assets` остаются публичными.**
6. **JWT живёт до 30 дней.** Web перепроверяет статус, прямой API — нет.
7. **Ведущие нули номера группы нормализуются** (`01` → `1`).
8. **Нет пагинации/keyset** для части списков и N+1 в отдельных экранах.
9. **Runtime/mutation/browser E2E не входят в unit-CI**; это release-скрипты.
без SLA production; 125 чтений и p95 < 1 с — ориентир, а не гарантия.
5. **Offset-пагинация аудита.** Вставки между страницами могут сдвигать
выдачу; keyset-курсор не реализован.
6. **N+1 в отдельных лабораторных списках/ведомости.** Имена групп курса и
аудит исправлены, но не все большие списки собраны пакетно.
7. **Нет абсолютного лимита web-сессии.** Sliding-сессия может продлеваться
бесконечно при непрерывной активности; отзыв по статусу при этом работает.
8. **Полный release-evidence не на каждом коммите.** На push/MR идёт smoke,
а mutation/browser/scale — для тегов или вручную.
9. **Внешняя интеграция «Контингент» не проверялась** — она остаётся заделом.
10. **Номер группы числовой.** Отдельного требования к ведущим нулям нет;
`01` и `1` — одно значение, это не открытый риск.
## 5. Вывод
Текущая функциональность прошла LDD-контур: **37/37 claims proven, P0 gaps = 0**,
273 теста, upgrade, mutation baseline и браузерный E2E зелёные. Это
достаточное основание продолжать доработки, но не «формальное доказательство
всей системы».
Текущая функциональность прошла LDD-контур: **46/46 claims proven, P0 gaps = 0**,
**319 тестов**, upgrade, mutation baseline, browser E2E, smoke и масштабный load
зелёные. Это достаточное основание продолжать доработки, но не «формальное
доказательство всей системы».
Следующий цикл LDD:
1. расширить browser E2E на публикацию оценки и остальные мобильные страницы;
2. добавить нагрузочный профиль с реалистичным объёмом групп/студентов;
3. закрыть известные продуктовые риски: legacy-активы, JWT 30 дней, ведущие
нули, пагинация/N+1;
4. сделать release-evidence job обязательным для тегов.
1. почистить инвентарь legacy-активов через `GET /api/v1/content/legacy-assets`
(файлы уже не публичны, это гигиена хранилища);
2. добавить keyset-пагинацию аудита и убрать оставшийся N+1 в лабораторных
списках/ведомости;
3. при необходимости ввести абсолютный лимит web-сессии;
4. расширить browser E2E на публикацию оценки и остальные мобильные страницы;
5. подготовить внешнюю интеграцию «Контингент»/`next.uniyar.ac.ru`.

View File

@@ -232,14 +232,14 @@
"criticality": "P0",
"status": "active",
"requirement": "docs/07-groups-courses.md, §6",
"claim": "Скрытый курсовой актив не отдаётся студенту, но доступен преподавателю; чужой курс получает 403; legacy без связи публичен.",
"claim": "Скрытый курсовой актив не отдаётся студенту, но доступен преподавателю; чужой курс получает 403; публичный глобальный ALL-актив доступен без входа.",
"oracle": "HTTP-статус и содержимое файла",
"evidence": [
"ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#hidden course asset is not served to student but is served to teacher()",
"ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#foreign teacher and student are forbidden for course asset()",
"ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#legacy asset without article association stays public()"
"ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#published global ALL asset stays public()"
],
"limitations": "Legacy-файлы без article_assets остаются публичными — отдельный остаточный риск."
"limitations": "Legacy-файлы без article_assets теперь закрыты отдельным claim LDD-ASSET-002."
},
{
"id": "LDD-MIG-001",
@@ -365,7 +365,7 @@
"ru.ysu.lms.identity.roster.StudentImportParserTest#parses new template and splits full name()",
"ru.ysu.lms.identity.roster.StudentImportParserTest#reports duplicate student number()"
],
"limitations": "Старый формат не поддерживается; ведущие нули нормализуются."
"limitations": "Старый формат импорта не поддерживается; номер группы числовой, отдельного требования к ведущим нулям нет."
},
{
"id": "LDD-LAB-002",
@@ -435,7 +435,7 @@
"criticality": "P1",
"status": "active",
"requirement": "docs/16-stage6-report.md, §2.2",
"claim": "Схема master + синтетические данные → миграции identity V4–V10, academic V6–V12, progress V13; V10 audit создаётся, V13 идемпотентна.",
"claim": "Схема master + синтетические данные → миграции identity V4–V11, academic V6–V12, progress V13; V10 audit и V11 refresh создаются, V13 идемпотентна.",
"oracle": "Успешное применение и postflight-наличие таблиц/колонок/индексов",
"runtime": [
"migration.master_upgrade"
@@ -538,6 +538,137 @@
"ru.ysu.lms.identity.promotion.PromotionApplyRollbackPropertyTest#generated cohorts apply to plan and rollback to before-state()"
],
"limitations": "10 детерминированных сценариев в одной транзакции; не заменяет property-генератор по всем полям."
},
{
"id": "LDD-RUNTIME-003",
"title": "Масштабный load: 100 групп и 1000 студентов",
"criticality": "P1",
"status": "active",
"requirement": "docs/evidence/README.md",
"claim": "После seed 100 групп/1000 студентов 125 параллельных чтений курса, лаб, профиля, групп и аудита дают 0 ошибок и p95 < 2 c.",
"oracle": "runtime.scale_seed + runtime.load_gradebook_profile",
"runtime": [
"runtime.scale_seed",
"runtime.load_gradebook_profile"
],
"limitations": "Синтетические данные в одноразовой БД; это smoke, не SLA production."
},
{
"id": "LDD-ASSET-002",
"title": "Файлы без привязки к статье не публичны",
"criticality": "P0",
"status": "active",
"requirement": "docs/15-stage5-course-materials.md",
"claim": "Аноним, студент и преподаватель получают 404 для legacy-актива без article_assets; админ получает файл; endpoint legacy-assets перечисляет такие файлы до привязки.",
"oracle": "HTTP 404/200 и список unlinkedAssetNames",
"evidence": [
"ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#legacy asset without article association is not public()",
"ru.ysu.lms.academic.content.ContentAssetAccessIntegrationTest#unlinkedAssetNames lists stored file until it is linked()"
],
"limitations": "Старые внешние ссылки на бесхозные файлы перестают работать у обычных пользователей — осознанный security-компромисс."
},
{
"id": "LDD-AUTH-001",
"title": "Короткий access-токен и ротация refresh",
"criticality": "P0",
"status": "active",
"requirement": "docs/16-stage6-report.md, §4",
"claim": "Login выдаёт access ≤15 минут и refresh; refresh ротируется, старый refresh отклоняется, повторное использование отзывает активные сессии.",
"oracle": "TTL JWT, ответы refresh, отзыв сессий",
"evidence": [
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#login returns short lived access token and refresh token()",
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#refresh rotates the token and old token is rejected()",
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#reuse of a rotated token revokes every active session()"
],
"limitations": "Refresh-токен хранится как SHA-256 отпечаток; очистка истёкших записей выполняется позже."
},
{
"id": "LDD-AUTH-002",
"title": "Отзыв refresh при logout/смене пароля и закрытом статусе",
"criticality": "P0",
"status": "active",
"requirement": "docs/16-stage6-report.md, §4",
"claim": "Logout и смена пароля отзывают refresh; отчисленный/архивный студент, архивный аккаунт и закрытая группа не могут обновить сессию.",
"oracle": "401 на refresh после отзыва/закрытия",
"evidence": [
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#logout revokes the refresh token idempotently()",
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#password change revokes refresh tokens()",
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#expelled student cannot refresh()",
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#archived account cannot refresh()",
"ru.ysu.lms.identity.RefreshTokenIntegrationTest#student in archived group cannot refresh()"
],
"limitations": "Access-токен живёт до 15 минут после отзыва refresh — это принятый компромисс."
},
{
"id": "LDD-AUTH-003",
"title": "Web-сессия 7 дней sliding и автообновление",
"criticality": "P1",
"status": "active",
"requirement": "docs/evidence/README.md",
"claim": "Web хранит refresh и expiry, обновляет access за 2 минуты до истечения, продлевает cookie на каждом ответе; logout отзывает refresh; identity outage не разлогинивает всех.",
"oracle": "SessionTokenService/AuthPageController/фильтр cookie",
"evidence": [
"ru.ysu.lms.web.session.SessionTokenServiceTest#token expiring soon is refreshed and session is updated()",
"ru.ysu.lms.web.session.SessionTokenServiceTest#identity outage fails open()",
"ru.ysu.lms.web.session.SessionCookieRefreshFilterTest#refreshes cookie max age on every existing session()",
"ru.ysu.lms.web.auth.AuthPageControllerTest#logout revokes refresh token and invalidates session()"
],
"limitations": "Cookie и серверный timeout оба 7 дней; абсолютного верхнего предела сессии нет."
},
{
"id": "LDD-AUDIT-004",
"title": "Пагинация журнала аудита",
"criticality": "P1",
"status": "active",
"requirement": "docs/evidence/README.md",
"claim": "Audit API отдаёт страницы limit/page без пересечений и с устойчивым порядком; UI показывает «Показать ещё».",
"oracle": "Страницы AuditIntegrationTest и ссылки шаблона",
"evidence": [
"ru.ysu.lms.identity.audit.AuditIntegrationTest#pagination returns disjoint pages newest first()",
"ru.ysu.lms.web.admin.AdminAuditTemplateTest#renders audit events with labels and filter()"
],
"limitations": "Offset-пагинация, не keyset; при частых вставках возможны сдвиги."
},
{
"id": "LDD-PERF-001",
"title": "Bulk-группы курса без N+1",
"criticality": "P1",
"status": "active",
"requirement": "docs/evidence/README.md",
"claim": "Страница преподавателя получает названия всех привязанных групп одним запросом; чужой преподаватель получает 403.",
"oracle": "Один вызов courseGroups и отсутствие courseGroupStudents на страницу",
"evidence": [
"ru.ysu.lms.academic.course.CourseGroupTeacherAccessTest#teacher gets attached groups with names in one call()",
"ru.ysu.lms.web.teacher.TeacherControllerCourseGroupsTest#loads group names with a single call instead of per-group requests()"
],
"limitations": "Детали лабораторных профиля всё ещё подтягиваются по одной; это следующий шаг."
},
{
"id": "LDD-AUTH-004",
"title": "Gateway пропускает refresh/logout без access-токена",
"criticality": "P0",
"status": "active",
"requirement": "docs/16-stage6-report.md, §4",
"claim": "POST /api/v1/auth/refresh и /api/v1/auth/logout проходят через gateway без access-JWT, иначе автообновление и отзыв сессии невозможны.",
"oracle": "GatewayFilterChain вызван для public path; protected path без токена — 401",
"evidence": [
"ru.ysu.lms.gateway.security.JwtAuthenticationFilterTest#refresh and logout are public()",
"ru.ysu.lms.gateway.security.JwtAuthenticationFilterTest#protected route without bearer token is rejected()"
],
"limitations": "Проверена маршрутизация gateway; интеграционный E2E с истёкшим access-токеном добавлен через web-тесты."
},
{
"id": "LDD-AUTH-005",
"title": "Refresh/logout работают через реальный gateway",
"criticality": "P0",
"status": "active",
"requirement": "docs/16-stage6-report.md, §4",
"claim": "Через gateway refresh без access-токена выдаёт новую пару, новый access работает, logout отзывает refresh, повторный refresh — 401.",
"oracle": "runtime.auth_refresh",
"runtime": [
"runtime.auth_refresh"
],
"limitations": "Проверено на одноразовом стеке; TTL не истекает в рамках smoke."
}
]
}