docs: закрытие продуктовых рисков и новый LDD baseline
This commit is contained in:
25
README.md
25
README.md
@@ -54,7 +54,8 @@ progress `:8083`, web `:8090`. Интерфейс: `http://localhost:8090`.
|
|||||||
./gradlew ktlintCheck detekt test # линтеры (строгий режим) + тесты
|
./gradlew ktlintCheck detekt test # линтеры (строгий режим) + тесты
|
||||||
./gradlew evidenceCheck # LDD: P0-claims должны иметь зелёные свидетельства
|
./gradlew evidenceCheck # LDD: P0-claims должны иметь зелёные свидетельства
|
||||||
./gradlew ktlintFormat # автоформат Kotlin
|
./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-migration-upgrade.sh # upgrade с master-схемы
|
||||||
./scripts/ldd-mutation.sh # PIT: mutation strength критичных P0-компонентов
|
./scripts/ldd-mutation.sh # PIT: mutation strength критичных P0-компонентов
|
||||||
./scripts/ldd-browser.sh # Playwright E2E (cd e2e && npm install && npx playwright install chromium)
|
./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`).
|
- Тестовая инфраструктура — `libs:test-support` (JUnit 5, MockK, Testcontainers `postgres:18`).
|
||||||
- **LDD** — реестр проверяемых утверждений `docs/evidence/claims.json`;
|
- **LDD** — реестр проверяемых утверждений `docs/evidence/claims.json`;
|
||||||
P0 без зелёного теста валит `./gradlew evidenceCheck`. Runtime, migration,
|
P0 без зелёного теста валит `./gradlew evidenceCheck`. Runtime, migration,
|
||||||
mutation и browser E2E — отдельные скрипты из `scripts/`; baseline —
|
mutation, browser E2E и smoke — скрипты из `scripts/`; baseline —
|
||||||
`docs/evidence/baseline-2026-10-27.md`.
|
`docs/evidence/baseline-2026-10-27.md`.
|
||||||
- CI merge gate — `.gitlab-ci.yml`: `./gradlew --no-daemon test ktlintCheck detekt evidenceCheck`
|
- CI merge gate — `.gitlab-ci.yml`: `test ktlintCheck detekt evidenceCheck` и
|
||||||
на push/MR. Runner должен разрешать privileged `docker:dind` для Testcontainers.
|
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 (для сервера)
|
## Запуск в Docker (для сервера)
|
||||||
|
|
||||||
@@ -113,6 +118,11 @@ cp .env.prod.example .env.prod # заполнить LMS_DB_PASSWORD
|
|||||||
- **Доступ**: JWT (RSA, ключи identity генерирует при первом старте и хранит в БД),
|
- **Доступ**: JWT (RSA, ключи identity генерирует при первом старте и хранит в БД),
|
||||||
проверка токена на gateway, роли ADMIN/TEACHER/STUDENT, CSRF на всех POST-формах,
|
проверка токена на gateway, роли ADMIN/TEACHER/STUDENT, CSRF на всех POST-формах,
|
||||||
session fixation-защита; вход студента с обязательной сменой пароля.
|
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` по шаблону `поток/курс/номер/степень/год набора/ФИО/№/база`,
|
импорт `.xls/.xlsx/.csv` по шаблону `поток/курс/номер/степень/год набора/ФИО/№/база`,
|
||||||
ручное добавление студента (генерация пароля) и «Добавить существующего»
|
ручное добавление студента (генерация пароля) и «Добавить существующего»
|
||||||
@@ -139,8 +149,8 @@ cp .env.prod.example .env.prod # заполнить LMS_DB_PASSWORD
|
|||||||
`/teacher/courses/{id}/content`. Чужой курс отклоняется на сервисах.
|
`/teacher/courses/{id}/content`. Чужой курс отклоняется на сервисах.
|
||||||
- **Аудит**: чувствительные действия (профили, скачивания, оценки, защиты,
|
- **Аудит**: чувствительные действия (профили, скачивания, оценки, защиты,
|
||||||
курсы, группы, материалы, преподаватели, жизненный цикл) пишутся в
|
курсы, группы, материалы, преподаватели, жизненный цикл) пишутся в
|
||||||
`identity.audit_events`; админ смотрит `/admin/audit` с фильтром по действию
|
`identity.audit_events`; админ смотрит `/admin/audit` с фильтром по действию,
|
||||||
и временем МСК.
|
временем МСК и постраничным переходом «Показать ещё» (`page`/`limit`).
|
||||||
- **Повышение и жизненный цикл**: `/admin/promotion` — dry-run, применение
|
- **Повышение и жизненный цикл**: `/admin/promotion` — dry-run, применение
|
||||||
и откат повышения (автоматический dry-run 31 августа МСК); на странице
|
и откат повышения (автоматический dry-run 31 августа МСК); на странице
|
||||||
группы — академотпуск, возврат, отчисление, архив и восстановление;
|
группы — академотпуск, возврат, отчисление, архив и восстановление;
|
||||||
@@ -165,6 +175,9 @@ cp .env.prod.example .env.prod # заполнить LMS_DB_PASSWORD
|
|||||||
markdown (код, спойлеры, картинки; санитизация OWASP); импорт ZIP
|
markdown (код, спойлеры, картинки; санитизация OWASP); импорт ZIP
|
||||||
(`chapters/N-глава/N-раздел/N-статья.md` + `assets/`); создание/переименование/
|
(`chapters/N-глава/N-раздел/N-статья.md` + `assets/`); создание/переименование/
|
||||||
удаление глав, разделов, статей; перенос статей между разделами.
|
удаление глав, разделов, статей; перенос статей между разделами.
|
||||||
|
Видимость картинок наследуется от статьи; файл без привязки к статье отдаёт
|
||||||
|
`404` обычным пользователям и доступен только админу, который получает
|
||||||
|
список таких файлов через `GET /api/v1/content/legacy-assets`.
|
||||||
- **Студент**: дашборд успеваемости (сдано, средний балл, лучшая/слабая оценка),
|
- **Студент**: дашборд успеваемости (сдано, средний балл, лучшая/слабая оценка),
|
||||||
карточки опубликованных лаб, страница лабы: условие, свой вариант, сдача
|
карточки опубликованных лаб, страница лабы: условие, свой вариант, сдача
|
||||||
файлов (zip/.c, переотправка до дедлайна, история, скачивание последней),
|
файлов (zip/.c, переотправка до дедлайна, история, скачивание последней),
|
||||||
|
|||||||
@@ -57,6 +57,22 @@ Compose передаёт одно и то же значение identity, academ
|
|||||||
Значение по умолчанию `dev-internal-token` для production не используйте;
|
Значение по умолчанию `dev-internal-token` для production не используйте;
|
||||||
`.env.prod` не коммитьте.
|
`.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. Собрать и запустить
|
## 4. Собрать и запустить
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -109,16 +125,24 @@ git pull
|
|||||||
|
|
||||||
`deploy.sh up` **обязательно** делает backup перед обновлением. Если backup
|
`deploy.sh up` **обязательно** делает backup перед обновлением. Если backup
|
||||||
завершился ошибкой, деплой прерывается. Миграции БД (Flyway) применяются
|
завершился ошибкой, деплой прерывается. Миграции БД (Flyway) применяются
|
||||||
автоматически при старте сервисов: identity `V4–V10` (`V10` создаёт
|
автоматически при старте сервисов: identity `V4–V11` (`V10` создаёт
|
||||||
`identity.audit_events`), academic `V6–V12`, progress `V13`.
|
`identity.audit_events`, `V11` — `identity.refresh_tokens`), academic `V6–V12`,
|
||||||
|
progress `V13`.
|
||||||
|
|
||||||
После обновления проверьте, что админ открывает `/admin/audit` и видит события
|
После обновления проверьте, что админ открывает `/admin/audit` и видит события
|
||||||
(например, после входа в профиль студента — `PROFILE_VIEW`); преподаватель и
|
(например, после входа в профиль студента — `PROFILE_VIEW`); преподаватель и
|
||||||
студент на эту страницу не попадают.
|
студент на эту страницу не попадают. Журнал листается через `page`/`limit`
|
||||||
|
(«Показать ещё»). Для инвентаризации старых файлов админ может вызвать
|
||||||
|
`GET /api/v1/content/legacy-assets`: файлы без привязки к статье больше не
|
||||||
|
публичны (`404` для обычных пользователей), их нужно привязать или удалить.
|
||||||
|
|
||||||
CI (`.gitlab-ci.yml`) на каждом push/MR выполняет
|
CI (`.gitlab-ci.yml`) на каждом push/MR выполняет
|
||||||
`./gradlew --no-daemon test ktlintCheck detekt`; для Testcontainers runner
|
`./gradlew --no-daemon test ktlintCheck detekt evidenceCheck` и job `smoke`
|
||||||
должен разрешать privileged `docker:dind`.
|
(`./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. Резервное копирование
|
## 8. Резервное копирование
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,11 @@ flowchart LR
|
|||||||
id -.->|JWKS| pr
|
id -.->|JWKS| pr
|
||||||
```
|
```
|
||||||
|
|
||||||
- Браузер никогда не получает JWT: токен живёт в HTTP-сессии web-сервиса
|
- Браузер никогда не получает JWT: access-токен живёт в HTTP-сессии web-сервиса
|
||||||
(`lms.token`), а в браузер уходит только cookie сессии (`HttpOnly`, 30 дней).
|
(`lms.token`), а в браузер уходит только cookie сессии `LMS_SESSION`
|
||||||
|
(`HttpOnly`, `SameSite=Lax`, 7 дней sliding). Access-токен — 15 минут,
|
||||||
|
обновляется автоматически за 2 минуты до истечения по refresh-токену;
|
||||||
|
cookie `Max-Age` перевыпускается на каждом ответе.
|
||||||
- Все REST-запросы web проксирует на gateway, добавляя `Authorization: Bearer`.
|
- Все REST-запросы web проксирует на gateway, добавляя `Authorization: Bearer`.
|
||||||
- Gateway проверяет подпись токена по JWKS identity и передаёт вниз заголовки
|
- Gateway проверяет подпись токена по JWKS identity и передаёт вниз заголовки
|
||||||
`X-Account-Id`, `X-Account-Login`, `X-Account-Role`.
|
`X-Account-Id`, `X-Account-Login`, `X-Account-Role`.
|
||||||
@@ -139,12 +142,13 @@ flowchart TD
|
|||||||
n -->|да| p["Контроллер → сервис → БД"]
|
n -->|да| p["Контроллер → сервис → БД"]
|
||||||
```
|
```
|
||||||
|
|
||||||
- Роли: `ADMIN`, `STUDENT`; конвертер ролей читает claim `role` как `ROLE_<role>`.
|
- Роли: `ADMIN`, `TEACHER`, `STUDENT`; конвертер ролей читает claim `role` как `ROLE_<role>`.
|
||||||
- Публичные пути: `/actuator/**`, `/api/v1/auth/{health,setup,login,jwks.json}`,
|
- Публичные пути: `/actuator/**`, `/api/v1/auth/{health,setup,login,refresh,jwks.json}`,
|
||||||
`/api/v1/content/tree`, `/api/v1/content/{chapter}/{section}/{article}`.
|
`/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 проверяет видимость статьи и для курсовых или
|
`ALL`-активы), но academic проверяет видимость статьи и для курсовых или
|
||||||
ролевых активов требует токен.
|
ролевых активов требует токен. Файл без привязки к статье — `404` всем,
|
||||||
|
кроме админа (secure-by-default).
|
||||||
- RSA-ключи identity хранятся в таблице `settings` и публикуются в JWKS.
|
- RSA-ключи identity хранятся в таблице `settings` и публикуются в JWKS.
|
||||||
|
|
||||||
## 5. Рендеринг страниц и защита форм (web)
|
## 5. Рендеринг страниц и защита форм (web)
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ flowchart TB
|
|||||||
`LMS_JWT_JWK_SET_URI` обязателен для gateway, academic и progress —
|
`LMS_JWT_JWK_SET_URI` обязателен для gateway, academic и progress —
|
||||||
в контейнерах это `http://identity:8081/api/v1/auth/jwks.json` (иначе 401).
|
в контейнерах это `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. Сборка
|
## 3. Сборка
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
@@ -101,7 +105,7 @@ sequenceDiagram
|
|||||||
DC->>DC: docker build (5 стадий)
|
DC->>DC: docker build (5 стадий)
|
||||||
DC->>PG: старт postgres (volume lms_pgdata)
|
DC->>PG: старт postgres (volume lms_pgdata)
|
||||||
DC->>DC: старт identity → academic → progress → gateway → web
|
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`.
|
- Локально (`docker-compose.yml`) поднимается только `postgres`; прод-стек — `docker-compose.prod.yml`.
|
||||||
|
|||||||
@@ -128,7 +128,9 @@ printf("Ответ: 42\n");
|
|||||||
* Ссылки вида `assets/имя` автоматически заменяются на адрес
|
* Ссылки вида `assets/имя` автоматически заменяются на адрес
|
||||||
`/api/v1/content/assets/...`; сами файлы складываются в хранилище. Для
|
`/api/v1/content/assets/...`; сами файлы складываются в хранилище. Для
|
||||||
курсовых статей этот адрес требует сессию пользователя курса, для публичных
|
курсовых статей этот адрес требует сессию пользователя курса, для публичных
|
||||||
`ALL`-материалов и старых несвязанных файлов остаётся публичным.
|
`ALL`-материалов остаётся публичным. Файл без привязки к статье больше не
|
||||||
|
публичен: обычные пользователи получают `404`, админ — файл и его имя в
|
||||||
|
`GET /api/v1/content/legacy-assets` для привязки или удаления.
|
||||||
|
|
||||||
## Публикация и скрытие
|
## Публикация и скрытие
|
||||||
|
|
||||||
|
|||||||
@@ -60,8 +60,8 @@
|
|||||||
|
|
||||||
**Разбор имени группы:** `^(.+)-(\d)(\d+)(БО|МО)$` → `stream` (буквы/дефисы), `course` (одна цифра),
|
**Разбор имени группы:** `^(.+)-(\d)(\d+)(БО|МО)$` → `stream` (буквы/дефисы), `course` (одна цифра),
|
||||||
`number` (остальные цифры), `degree`. Примеры: `ПИ-11БО` → ПИ/1/1/БО; `ИВТ-ТОП-12БО` → ИВТ-ТОП/1/2/БО;
|
`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`), чтение —
|
из сервисов через `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
|
### academic
|
||||||
|
|
||||||
@@ -400,8 +422,30 @@ V13 идемпотентна: если старая feature-миграция V10
|
|||||||
и защиты. Свой профиль студент открывает без записи в аудит, чтобы не
|
и защиты. Свой профиль студент открывает без записи в аудит, чтобы не
|
||||||
засорять журнал и не нагружать identity на горячем пути. Админ смотрит
|
засорять журнал и не нагружать identity на горячем пути. Админ смотрит
|
||||||
журнал на `/admin/audit` с фильтром по действию; время отображается в МСК.
|
журнал на `/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`; глобальные публикует только админ.
|
||||||
- Нужны **публичные маршруты** для `ALL` без аутентификации. `STUDENTS`/`TEACHERS`/курсовые материалы
|
- Нужны **публичные маршруты** для `ALL` без аутентификации. `STUDENTS`/`TEACHERS`/курсовые материалы
|
||||||
анонимно недоступны ни при каких условиях.
|
анонимно недоступны ни при каких условиях.
|
||||||
- **Активы (риск):** материалы ссылаются на файлы (`/api/v1/assets/**`). Для публичных глав активы должны
|
- **Активы (реализовано):** видимость актива наследуется от статьи → раздела →
|
||||||
быть доступны, для приватных — нет. Решение: видимость актива наследуется от главы, и маршрут выдачи
|
главы, маршрут `/api/v1/content/assets/{name}` проверяет доступ. Файл без
|
||||||
активов обязан это проверять (либо отдельный публичный префикс для `ALL`). Без этого публичные материалы
|
связи со статьёй не публичен: аноним/студент/преподаватель получают `404`,
|
||||||
будут ломаться, а приватные — утекать.
|
админ может скачать файл и получить список таких файлов через
|
||||||
|
`GET /api/v1/content/legacy-assets` для привязки или удаления. Старые
|
||||||
|
внешние ссылки на несвязанные файлы у обычных пользователей перестают
|
||||||
|
работать — это осознанное secure-by-default поведение.
|
||||||
- Поисковая индексация: для `ALL` — по умолчанию разрешена; для остального — закрыть (robots/noindex).
|
- Поисковая индексация: для `ALL` — по умолчанию разрешена; для остального — закрыть (robots/noindex).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# 08. План реализации: группы, курсы, преподаватели
|
# 08. План реализации: группы, курсы, преподаватели
|
||||||
|
|
||||||
**Статус:** план согласован, этапы 0–6 выполнены. Этап 6 закрыт: права
|
**Статус:** план согласован, этапы 0–6 выполнены. Этап 6 закрыт: права
|
||||||
преподавателя на запись, аудит и CI добавлены; остаточные риски — § 9.1 и
|
преподавателя на запись, аудит и CI добавлены; продуктовые риски (legacy-активы,
|
||||||
[16-stage6-report.md](16-stage6-report.md).
|
срок жизни JWT/сессии, пагинация аудита) закрыты 2026-10-28. Оставшиеся
|
||||||
|
ограничения — § 9.1 и [16-stage6-report.md](16-stage6-report.md).
|
||||||
**Основа:** [07-groups-courses.md](07-groups-courses.md).
|
**Основа:** [07-groups-courses.md](07-groups-courses.md).
|
||||||
**Дата решений:** 2026-10-23.
|
**Дата решений:** 2026-10-23.
|
||||||
|
|
||||||
@@ -195,10 +196,12 @@
|
|||||||
привязка групп, авторские материалы курса.
|
привязка групп, авторские материалы курса.
|
||||||
- Полная security-матрица и negative-тесты «свой/чужой курс».
|
- Полная security-матрица и negative-тесты «свой/чужой курс».
|
||||||
- Upgrade-тест миграций.
|
- Upgrade-тест миграций.
|
||||||
- Индексы; пагинация и N+1 — follow-up (§ 9.1).
|
- Индексы; пагинация аудита и bulk-выдача групп с именами — выполнены;
|
||||||
|
оставшийся N+1 в отдельных лабораторных списках — follow-up (§ 9.1).
|
||||||
- Аудит скачиваний и чувствительных действий (`identity.audit_events`,
|
- Аудит скачиваний и чувствительных действий (`identity.audit_events`,
|
||||||
`/admin/audit`).
|
`/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.
|
- Проверка backup: создание, retention, `verify`, restore на staging.
|
||||||
- Документация, deploy/runbook, smoke.
|
- Документация, deploy/runbook, smoke.
|
||||||
|
|
||||||
@@ -208,11 +211,11 @@
|
|||||||
|
|
||||||
Номера уточняются при реализации, чтобы не конфликтовать с максимумами master
|
Номера уточняются при реализации, чтобы не конфликтовать с максимумами master
|
||||||
(identity V3, **academic V5**, progress V12). Максимумы этой ветки:
|
(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 |
|
| 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` |
|
| 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 до кода.
|
- **Межсервисная авторизация.** Самый сложный участок; ADR до кода.
|
||||||
- **Перенос данных.** Нужны preflight и копия БД; группы с нестандартными именами
|
- **Перенос данных.** Нужны preflight и копия БД; группы с нестандартными именами
|
||||||
и дубли номеров лаб могут заблокировать `NOT NULL`.
|
и дубли номеров лаб могут заблокировать `NOT NULL`.
|
||||||
- **Legacy-активы.** Связанные активы наследуют видимость статьи (миграция `V12`),
|
- **Legacy-активы.** Закрыто: связанные активы наследуют видимость статьи
|
||||||
но файлы без markdown-ссылки остаются публичными; нужна инвентаризация и
|
(миграция `V12`), а файлы без markdown-ссылки по умолчанию отдают `404`
|
||||||
привязка или удаление.
|
анониму/студенту/преподавателю. Админ перечисляет их через
|
||||||
- **JWT 30 дней.** Средний вариант блокировки оставляет остаточный риск для
|
`GET /api/v1/content/legacy-assets` для привязки или удаления.
|
||||||
прямого API; фиксируем в документации.
|
- **JWT и сессии.** Закрыто: access-токен 15 минут, refresh 7 дней с ротацией
|
||||||
|
и отзывом, web-сессия 7 дней sliding. Прямые API-клиенты используют
|
||||||
|
`POST /api/v1/auth/refresh`; logout и смена пароля отзывают refresh.
|
||||||
|
- **Оставшаяся производительность.** Аудит постраничный и группы курса
|
||||||
|
отдаются одним вызовом, но N+1 в отдельных лабораторных списках и
|
||||||
|
offset-пагинация аудита остаются лабораторными деталями.
|
||||||
- **Комментарии без автора.** Старые записи останутся с «автор не указан».
|
- **Комментарии без автора.** Старые записи останутся с «автор не указан».
|
||||||
- **Архивные студенты.** Открывать ли профиль после отвязки группы — уточнить
|
- **Архивные студенты.** Открывать ли профиль после отвязки группы — уточнить
|
||||||
на этапе 3; по умолчанию история курса доступна, если студент был в курсе.
|
на этапе 3; по умолчанию история курса доступна, если студент был в курсе.
|
||||||
@@ -326,24 +334,25 @@ identity V10, **academic V12**, progress V13.
|
|||||||
- **Этап 6 — выполнен.** Права преподавателя на запись (лабораторные,
|
- **Этап 6 — выполнен.** Права преподавателя на запись (лабораторные,
|
||||||
ведомость, группы, материалы), центральный аудит `identity.audit_events`
|
ведомость, группы, материалы), центральный аудит `identity.audit_events`
|
||||||
с `/admin/audit`, CI merge gate и полный hardening. Проверены прогон
|
с `/admin/audit`, CI merge gate и полный hardening. Проверены прогон
|
||||||
тестов/линтеров (**267**), upgrade миграций с master-схемы (identity
|
тестов/линтеров (**319**, 0 failures), upgrade миграций с master-схемы
|
||||||
`V4–V10`, academic `V6–V12`, progress `V13`), security-матрица, gateway,
|
(identity `V4–V11`, academic `V6–V12`, progress `V13`), security-матрица,
|
||||||
видимость активов и deploy-runbook. Отчёт: [16-stage6-report.md](16-stage6-report.md).
|
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
|
### 9.1. Известные пробелы после этапа 6
|
||||||
|
|
||||||
- **Legacy-активы без привязки.** Маршрут `/api/v1/content/assets/{name}`
|
- **Импорт.** Старый формат не поддерживается, принимается только шаблон v2.
|
||||||
проверяет `article_assets → article → section → chapter`, а `V12`
|
Номер группы — числовой; отдельного требования к ведущим нулям нет.
|
||||||
восстанавливает связи из markdown. Файлы без ссылок в статьях остаются
|
- **Пагинация и N+1.** `GET /api/v1/audit` поддерживает `page` + `limit`,
|
||||||
публичными; нужна инвентаризация и привязка или удаление.
|
в UI есть «Показать ещё»; `GET /api/v1/courses/{id}/groups` отдаёт
|
||||||
- **Импорт.** Старый формат не поддерживается; ведущие нули номера группы
|
привязанные группы с именами одним вызовом. Остаются offset-пагинация
|
||||||
нормализуются (`01` → `1`).
|
аудита и N+1 в отдельных лабораторных списках/ведомости на больших курсах.
|
||||||
- **JWT 30 дней.** Web перепроверяет статус каждые 45 секунд, но прямой API
|
- **Абсолютный лимит сессии.** Web-сессия скользящая (7 дней) и отзывается
|
||||||
с уже выданным токеном живёт до истечения; нужны короткий TTL + refresh
|
при выходе/смене пароля/закрытии доступа, но при непрерывной активности
|
||||||
или revocation.
|
может жить бессрочно; при необходимости нужен абсолютный `max-age`.
|
||||||
- **Пагинация и N+1.** Журнал аудита ограничен `limit` без offset/keyset;
|
- **Внешняя интеграция.** «Контингент»/`next.uniyar.ac.ru` остаётся заделом:
|
||||||
часть списков групп, материалов и ведомости отдаётся целиком; на больших
|
отдельный anti-corruption слой и внешние идентификаторы — позже.
|
||||||
объёмах нужны пагинация и пакетные выборки.
|
|
||||||
|
|
||||||
## 10. Definition of Done
|
## 10. Definition of Done
|
||||||
|
|
||||||
@@ -354,8 +363,13 @@ identity V10, **academic V12**, progress V13.
|
|||||||
- Профиль показывает статистику, комментарии и все попытки; файлы скачиваются.
|
- Профиль показывает статистику, комментарии и все попытки; файлы скачиваются.
|
||||||
- Повышение, академотпуск, выпуск и архив работают по ТЗ.
|
- Повышение, академотпуск, выпуск и архив работают по ТЗ.
|
||||||
- Аудит чувствительных действий пишется в `identity.audit_events` и доступен
|
- Аудит чувствительных действий пишется в `identity.audit_events` и доступен
|
||||||
админу на `/admin/audit`.
|
админу на `/admin/audit` с пагинацией.
|
||||||
- Security-матрица и миграционный upgrade-тест зелёные.
|
- Активы наследуют видимость материала; файлы без привязки не публичны и
|
||||||
- CI на каждом push/MR прогоняет `test ktlintCheck detekt`.
|
доступны только админу через инвентаризацию `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.
|
- Backup перед деплоем обязателен; restore проверен на staging.
|
||||||
- Документация, deploy/runbook и smoke обновлены.
|
- Документация, deploy/runbook и smoke обновлены.
|
||||||
|
|||||||
@@ -9,9 +9,10 @@
|
|||||||
- ADR с ключевыми решениями: [adr/0001-groups-courses-foundation.md](adr/0001-groups-courses-foundation.md).
|
- ADR с ключевыми решениями: [adr/0001-groups-courses-foundation.md](adr/0001-groups-courses-foundation.md).
|
||||||
- Контракт API профиля студента: [09-teacher-student-profile.md](09-teacher-student-profile.md).
|
- Контракт API профиля студента: [09-teacher-student-profile.md](09-teacher-student-profile.md).
|
||||||
- Миграции:
|
- Миграции:
|
||||||
- identity `V4–V10`: роли, группы-когорты, статусы студентов, повышение,
|
- identity `V4–V11`: роли, группы-когорты, статусы студентов, повышение,
|
||||||
история членства, статусы аккаунтов; `V10` (`audit_events`) добавлена
|
история членства, статусы аккаунтов; `V10` (`audit_events`) добавлена
|
||||||
позже, на этапе 6;
|
позже, на этапе 6; `V11` (`refresh_tokens`) — при закрытии рисков
|
||||||
|
аутентификации, тоже после этого отчёта;
|
||||||
- academic `V6–V12`: курсы, преподаватели, группы курса, `labs.course_id`,
|
- academic `V6–V12`: курсы, преподаватели, группы курса, `labs.course_id`,
|
||||||
материалы курса/аудитории, индексы активов; `V10` (уникальный индекс
|
материалы курса/аудитории, индексы активов; `V10` (уникальный индекс
|
||||||
`lower(title)` по курсам) добавлена на этапе 2, после этого отчёта;
|
`lower(title)` по курсам) добавлена на этапе 2, после этого отчёта;
|
||||||
|
|||||||
@@ -74,9 +74,9 @@
|
|||||||
## 3. Ограничения этапа
|
## 3. Ограничения этапа
|
||||||
|
|
||||||
- Импорт поддерживает одну группу на файл; мультигрупповой файл — позже.
|
- Импорт поддерживает одну группу на файл; мультигрупповой файл — позже.
|
||||||
- Ведущие нули номера группы нормализуются к числу (`01` → `1`);
|
- Номер группы числовой (`01` и `1` — одно значение). Отдельный строковый
|
||||||
имя строится из числового номера. Если нужны разные `ПИ-11БО` и `ПИ-101БО`
|
номер и различие `ПИ-11БО`/`ПИ-101БО` как разных когорт не требуются;
|
||||||
как отдельные когорты, потребуется хранить номер строкой.
|
имя группы по-прежнему строится из полей.
|
||||||
- Роль `TEACHER` уже есть в домене, но кабинет преподавателя и назначения
|
- Роль `TEACHER` уже есть в домене, но кабинет преподавателя и назначения
|
||||||
на курс — этапы 2–3.
|
на курс — этапы 2–3.
|
||||||
- `ACADEMIC_LEAVE` пока может входить; режим «только чтение» — этап 3.
|
- `ACADEMIC_LEAVE` пока может входить; режим «только чтение» — этап 3.
|
||||||
|
|||||||
@@ -168,6 +168,8 @@
|
|||||||
- **JWT и блокировка.** Вход закрывается при следующей аутентификации;
|
- **JWT и блокировка.** Вход закрывается при следующей аутентификации;
|
||||||
уже выданный JWT действует до истечения (до 30 дней). Это остаточный
|
уже выданный JWT действует до истечения (до 30 дней). Это остаточный
|
||||||
риск, зафиксированный в плане; аудит и hardening — этап 6.
|
риск, зафиксированный в плане; аудит и hardening — этап 6.
|
||||||
|
*Историческая оценка этапа 4; закрыто в identity `V11` (access 15 минут,
|
||||||
|
refresh 7 дней с отзывом) — см. [16](16-stage6-report.md).*
|
||||||
- **Нет новых миграций.** Используются таблицы `promotion_runs`,
|
- **Нет новых миграций.** Используются таблицы `promotion_runs`,
|
||||||
`promotion_run_items`, `membership_history`, `accounts.status`
|
`promotion_run_items`, `membership_history`, `accounts.status`
|
||||||
из миграций `V7`–`V9` этапа 1. JSONB пишется через `JdbcTemplate`
|
из миграций `V7`–`V9` этапа 1. JSONB пишется через `JdbcTemplate`
|
||||||
|
|||||||
@@ -36,7 +36,9 @@
|
|||||||
расширению и `size = 0`. Повторный запуск не создаёт дубликатов.
|
расширению и `size = 0`. Повторный запуск не создаёт дубликатов.
|
||||||
- Публичный API `/api/v1/content/assets/{name}` проверяет доступ по всем
|
- Публичный API `/api/v1/content/assets/{name}` проверяет доступ по всем
|
||||||
статьям, к которым привязан файл (достаточно одной видимой):
|
статьям, к которым привязан файл (достаточно одной видимой):
|
||||||
- актив без связей в `article_assets` — legacy-файл, публичен как раньше;
|
- актив без связей в `article_assets` — secure-by-default: `404` для
|
||||||
|
анонима, студента и преподавателя; только `ADMIN` может скачать файл для
|
||||||
|
инвентаризации/очистки;
|
||||||
- глобальная опубликованная статья `ALL` — публична, в том числе анонимно;
|
- глобальная опубликованная статья `ALL` — публична, в том числе анонимно;
|
||||||
- глобальные `STUDENTS`/`TEACHERS` — только с JWT и соответствующей ролью;
|
- глобальные `STUDENTS`/`TEACHERS` — только с JWT и соответствующей ролью;
|
||||||
- курсовой актив — только с JWT и доступом к курсу: админ видит любой курс,
|
- курсовой актив — только с JWT и доступом к курсу: админ видит любой курс,
|
||||||
@@ -47,12 +49,17 @@
|
|||||||
поэтому защищённые картинки на страницах `/learn/...` открываются, а на
|
поэтому защищённые картинки на страницах `/learn/...` открываются, а на
|
||||||
публичных страницах прокси остаётся анонимным.
|
публичных страницах прокси остаётся анонимным.
|
||||||
|
|
||||||
**Остаточный риск — legacy-активы без привязки.** `V12` закрывает только файлы,
|
## Legacy-активы без привязки (закрыто)
|
||||||
на которые есть ссылки в markdown. Если файл был загружен, но ни одна статья на
|
|
||||||
него не ссылается (осиротевший или удалённый из текста актив), он остаётся
|
`V12` закрывает файлы, на которые есть ссылки в markdown. Если файл был
|
||||||
публичным. Нужна инвентаризация таких файлов и привязка к статье либо удаление;
|
загружен, но ни одна статья на него не ссылается, он больше **не публичен**:
|
||||||
до этого поведение сохраняется осознанно, чтобы не ломать старые публичные
|
аноним, студент и преподаватель получают `404`, чтобы не раскрывать
|
||||||
ссылки.
|
существование файла. Админ скачивает такой файл и получает его имя в
|
||||||
|
инвентаризации `GET /api/v1/content/legacy-assets`, после чего привязывает
|
||||||
|
файл к статье или удаляет. Старые внешние ссылки на несвязанные файлы у
|
||||||
|
обычных пользователей перестают работать — это осознанное поведение
|
||||||
|
secure-by-default, а не регрессия. Эндпоинт инвентаризации защищён
|
||||||
|
`ROLE_ADMIN`.
|
||||||
|
|
||||||
## Авторство материалов преподавателем (добавлено на этапе 6)
|
## Авторство материалов преподавателем (добавлено на этапе 6)
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
# 16. Отчёт этапа 6: проверка, hardening и передача
|
# 16. Отчёт этапа 6: проверка, hardening и передача
|
||||||
|
|
||||||
**Дата:** 2026-10-27.
|
**Дата:** 2026-10-27, обновлено 2026-10-28 после закрытия продуктовых рисков.
|
||||||
**Ветка:** `feature/groups-courses`.
|
**Ветка:** `feature/groups-courses`.
|
||||||
**Статус:** этап 6 закрыт. Остаточные риски перечислены в разделе 4 и не
|
**Статус:** этап 6 закрыт. Продуктовые риски (legacy-активы, срок жизни JWT,
|
||||||
блокируют передачу.
|
сессии, пагинация аудита и N+1 имён групп) закрыты; оставшиеся ограничения
|
||||||
|
перечислены в разделе 4 и не блокируют передачу.
|
||||||
|
|
||||||
## 1. Итоговый объём этапов 0–6
|
## 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) |
|
| 1 | Роли `ADMIN/TEACHER/STUDENT`, группы-когорты и парсер имени, статусы студентов и аккаунтов, `membership_history`, импорт v2 | [11](11-stage1-report.md) |
|
||||||
| 2 | Курсы, `course_no` для браузерных URL, история преподавателей и групп, bootstrap курса C, привязка лабораторных к курсу, копирование лабораторных, админ-UI | [12](12-stage2-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) |
|
| 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 |
|
| progress-service | 33 | 0 | 0 | 0 |
|
||||||
| web | 69 | 0 | 0 | 0 |
|
| web | 69 | 0 | 0 | 0 |
|
||||||
| markdown | 5 | 0 | 0 | 0 |
|
| markdown | 5 | 0 | 0 | 0 |
|
||||||
| **Итого** | **267** | **0** | **0** | **0** |
|
| **Итого на момент этапа 6** | **267** | **0** | **0** | **0** |
|
||||||
|
|
||||||
`ktlintCheck` и `detekt` прошли без замечаний. Отдельных unit-тестов у gateway
|
После закрытия рисков актуальный LDD-baseline — **319 тестов, 0 failures**
|
||||||
и `common-http` в текущем дереве нет; маршрутизация gateway проверена
|
(см. раздел 7). `ktlintCheck` и `detekt` прошли без замечаний. У gateway теперь есть unit-тесты публичных auth-путей и 401; маршрутизация
|
||||||
конфигурацией и security-матрицей ниже. Тот же набор команд выполняет
|
gateway дополнительно проверена конфигурацией и security-матрицей ниже.
|
||||||
`.gitlab-ci.yml`.
|
Тот же набор команд выполняет `.gitlab-ci.yml`; на каждый push/MR добавлен
|
||||||
|
smoke-job, полный release-evidence обязателен для тегов.
|
||||||
|
|
||||||
### 2.2. Upgrade-тест миграций
|
### 2.2. Upgrade-тест миграций
|
||||||
|
|
||||||
@@ -81,7 +86,7 @@
|
|||||||
(`ПИ-11БО`, `ПИ-21БО`), зачисления, семестр, лабораторная, глава/раздел/
|
(`ПИ-11БО`, `ПИ-21БО`), зачисления, семестр, лабораторная, глава/раздел/
|
||||||
статья/актив, оценка, комментарий и попытка сдачи.
|
статья/актив, оценка, комментарий и попытка сдачи.
|
||||||
3. Поверх master применены миграции ветки:
|
3. Поверх master применены миграции ветки:
|
||||||
- identity `V4–V10`;
|
- identity `V4–V11`;
|
||||||
- academic `V6–V12`;
|
- academic `V6–V12`;
|
||||||
- progress `V13`.
|
- progress `V13`.
|
||||||
4. Выполнены postflight-запросы и повторное применение `V13`.
|
4. Выполнены postflight-запросы и повторное применение `V13`.
|
||||||
@@ -93,7 +98,9 @@
|
|||||||
курс 2, год 2025); статус `ACTIVE`; созданы `promotion_runs`,
|
курс 2, год 2025); статус `ACTIVE`; созданы `promotion_runs`,
|
||||||
`promotion_run_items`; зачисления перенесены в `membership_history`
|
`promotion_run_items`; зачисления перенесены в `membership_history`
|
||||||
действием `ENROLL`; добавлен `accounts.status`; `V10` создаёт
|
действием `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`,
|
- **academic:** добавлены `labs.course_id`, `chapters.course_id/audience`,
|
||||||
индексы `labs_number_course_uidx`, `chapters_slug_global_uidx`,
|
индексы `labs_number_course_uidx`, `chapters_slug_global_uidx`,
|
||||||
`chapters_slug_course_uidx`, `article_assets_article_idx`,
|
`chapters_slug_course_uidx`, `article_assets_article_idx`,
|
||||||
@@ -119,8 +126,8 @@
|
|||||||
|
|
||||||
### 2.3. Security-проверки
|
### 2.3. Security-проверки
|
||||||
|
|
||||||
Проверено автоматическими тестами (полный прогон 267 тестов) и разбором
|
Проверено автоматическими тестами (актуальный прогон — 319 тестов, 0 failures)
|
||||||
конфигурации gateway. Запись преподавателя дополнительно покрыта
|
и разбором конфигурации gateway. Запись преподавателя дополнительно покрыта
|
||||||
negative-тестами «свой/чужой курс».
|
negative-тестами «свой/чужой курс».
|
||||||
|
|
||||||
| Сценарий | Ожидание | Факт |
|
| Сценарий | Ожидание | Факт |
|
||||||
@@ -133,24 +140,28 @@ negative-тестами «свой/чужой курс».
|
|||||||
| Студент/преподаватель чужого курса → курсовой актив | `403` | `403` |
|
| Студент/преподаватель чужого курса → курсовой актив | `403` | `403` |
|
||||||
| Преподаватель своего курса → скрытый курсовой актив | доступ | `200`; админ видит любой актив |
|
| Преподаватель своего курса → скрытый курсовой актив | доступ | `200`; админ видит любой актив |
|
||||||
| Глобальный `STUDENTS`-актив → преподаватель | `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 |
|
| Преподаватель своего курса → запись | лабы/уровни/варианты, ведомость, группы, материалы | оценки/комментарии, публикация, уровни/варианты и раздача, защиты (код/reset), привязка групп, авторство материалов; проверки в progress `CourseScopeService` и academic course/content authoring |
|
||||||
| Преподаватель чужого курса | `403` | `403` на чтение и запись: материалы, курс по `course_no`, состав группы, профиль, лабы, оценки, группы |
|
| Преподаватель чужого курса | `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` |
|
| Аудит → запись из сервисов | только по токену | `POST /api/v1/internal/audit` с `X-Internal-Token`; без/неверный токен — `403` |
|
||||||
| Студент → свой профиль | только своё и опубликованное | скрытые оценки/комментарии отсутствуют; чужой `studentId` — `403` |
|
| Студент → свой профиль | только своё и опубликованное | скрытые оценки/комментарии отсутствуют; чужой `studentId` — `403` |
|
||||||
| Студент → чужой курс/группа | `403` | доступ только к курсам активной группы; архивный курс — `403` |
|
| Студент → чужой курс/группа | `403` | доступ только к курсам активной группы; архивный курс — `403` |
|
||||||
| Чужая сдача / лаба другого курса | `403` | скачивание отклоняется; своя попытка курса скачивается ZIP |
|
| Чужая сдача / лаба другого курса | `403` | скачивание отклоняется; своя попытка курса скачивается ZIP |
|
||||||
| Внутренние API identity | только по токену | неверный/пустой `X-Internal-Token` — `403`, сравнение constant-time; неизвестный id — `404` |
|
| Внутренние 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 |
|
| Gateway → заголовки личности | клиент не подменяет | `X-Account-Id/Login/Role` удаляются и проставляются только после проверки JWT |
|
||||||
| Web-сессия | быстрый отзыв доступа | статус перепроверяется не реже чем раз в 45 секунд; `active=false` или `401/403` инвалидируют сессию; недоступность identity не разлогинивает всех |
|
|
||||||
|
|
||||||
Остаточные риски перечислены в разделе 4: legacy-активы без привязки,
|
Остаточные ограничения перечислены в разделе 4: offset-пагинация аудита,
|
||||||
30-дневный JWT для прямого API, ведущие нули номера группы и отсутствие
|
N+1 в отдельных лабораторных списках и отсутствие абсолютного лимита
|
||||||
пагинации/N+1 на больших объёмах.
|
web-сессии. Продуктовые риски legacy-активов, 30-дневного JWT и ведущих
|
||||||
|
нулей номера группы закрыты или сняты как не относящиеся к требованиям.
|
||||||
|
|
||||||
## 3. Чек-лист деплоя
|
## 3. Чек-лист деплоя
|
||||||
|
|
||||||
@@ -166,18 +177,25 @@ negative-тестами «свой/чужой курс».
|
|||||||
Там же задан `LMS_DB_PASSWORD`; `.env.prod` не коммитится.
|
Там же задан `LMS_DB_PASSWORD`; `.env.prod` не коммитится.
|
||||||
- [ ] Проверить, что на сервере нет незакоммиченных изменений и выполнен
|
- [ ] Проверить, что на сервере нет незакоммиченных изменений и выполнен
|
||||||
`git pull` нужной ветки/тега.
|
`git pull` нужной ветки/тега.
|
||||||
- [ ] **CI зелёный.** `.gitlab-ci.yml` выполняет
|
- [ ] **CI зелёный.** `.gitlab-ci.yml` на каждом push/MR выполняет
|
||||||
`./gradlew --no-daemon test ktlintCheck detekt`. Runner должен разрешать
|
`test ktlintCheck detekt evidenceCheck` и job `smoke`
|
||||||
privileged-сервисы `docker:dind`: Testcontainers поднимает PostgreSQL 18
|
(`scripts/ldd-smoke.sh`: полный стек, health, вход админа, один курс).
|
||||||
через Docker-in-Docker.
|
Полный `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. Порядок выкладки
|
### 3.2. Порядок выкладки
|
||||||
|
|
||||||
1. **Flyway.** `./deploy/deploy.sh up` собирает образы и запускает
|
1. **Flyway.** `./deploy/deploy.sh up` собирает образы и запускает
|
||||||
`postgres → identity → academic → progress → gateway → web`. Миграции
|
`postgres → identity → academic → progress → gateway → web`. Миграции
|
||||||
применяются автоматически при старте сервисов:
|
применяются автоматически при старте сервисов:
|
||||||
identity `V4–V10` (`V10` — `identity.audit_events`), academic `V6–V12`,
|
identity `V4–V11` (`V10` — `identity.audit_events`, `V11` —
|
||||||
progress `V13`. Отдельный ручной запуск Flyway не нужен.
|
`identity.refresh_tokens`), academic `V6–V12`, progress `V13`.
|
||||||
|
Отдельный ручной запуск Flyway не нужен.
|
||||||
2. **Bootstrap курса C.** После входа админа открыть `/admin/courses`.
|
2. **Bootstrap курса C.** После входа админа открыть `/admin/courses`.
|
||||||
Пока курсов нет, показан блок первичной настройки. Bootstrap идемпотентно
|
Пока курсов нет, показан блок первичной настройки. Bootstrap идемпотентно
|
||||||
создаёт/находит курс «Промышленное программирование на языке C»,
|
создаёт/находит курс «Промышленное программирование на языке C»,
|
||||||
@@ -194,7 +212,8 @@ negative-тестами «свой/чужой курс».
|
|||||||
- ровно один курс C;
|
- ровно один курс C;
|
||||||
- `comments_without_author` соответствует старым комментариям;
|
- `comments_without_author` соответствует старым комментариям;
|
||||||
- группы без полного разбора отсутствуют;
|
- группы без полного разбора отсутствуют;
|
||||||
- таблица `identity.audit_events` существует (`V10`).
|
- таблица `identity.audit_events` существует (`V10`);
|
||||||
|
- таблица `identity.refresh_tokens` существует (`V11`).
|
||||||
4. **V10 → V13 — только dev/staging.** На production/master записи
|
4. **V10 → V13 — только dev/staging.** На production/master записи
|
||||||
`progress V10__profile_authors.sql` нет, ничего удалять не нужно. Не путать
|
`progress V10__profile_authors.sql` нет, ничего удалять не нужно. Не путать
|
||||||
с academic `V10__course_title_unique.sql` — он относится к другой схеме и
|
с academic `V10__course_title_unique.sql` — он относится к другой схеме и
|
||||||
@@ -229,7 +248,10 @@ negative-тестами «свой/чужой курс».
|
|||||||
только опубликованные; чужой курс даёт `403`, аноним уходит на `/login`.
|
только опубликованные; чужой курс даёт `403`, аноним уходит на `/login`.
|
||||||
- [ ] **Активы:** картинки курсовой статьи открываются у студента своего курса
|
- [ ] **Активы:** картинки курсовой статьи открываются у студента своего курса
|
||||||
и преподавателя; чужой курс — `403`, скрытая картинка у студента — `404`,
|
и преподавателя; чужой курс — `403`, скрытая картинка у студента — `404`,
|
||||||
публичная `ALL` — без входа; старый файл без привязки — публичен.
|
публичная `ALL` — без входа; файл без привязки к статье — `404` для всех,
|
||||||
|
кроме админа. Админ проверяет инвентаризацию через
|
||||||
|
`GET /api/v1/content/legacy-assets` (или админский UI/curl) и при
|
||||||
|
необходимости привязывает или удаляет файлы.
|
||||||
- [ ] **Копирование лабораторной:** админ копирует лабораторную в другой курс;
|
- [ ] **Копирование лабораторной:** админ копирует лабораторную в другой курс;
|
||||||
создаётся черновик с уровнями и вариантами; оценки, сдачи, защиты и
|
создаётся черновик с уровнями и вариантами; оценки, сдачи, защиты и
|
||||||
назначения не копируются; занятый номер даёт `409`.
|
назначения не копируются; занятый номер даёт `409`.
|
||||||
@@ -241,45 +263,48 @@ negative-тестами «свой/чужой курс».
|
|||||||
- [ ] **Аудит:** `/admin/audit` открывается у админа, показывает события
|
- [ ] **Аудит:** `/admin/audit` открывается у админа, показывает события
|
||||||
(например, `PROFILE_VIEW`, `SUBMISSION_DOWNLOAD`, `GRADE_SET`,
|
(например, `PROFILE_VIEW`, `SUBMISSION_DOWNLOAD`, `GRADE_SET`,
|
||||||
`CONTENT_*`, `TEACHER_*`) с фильтром по действию и временем МСК;
|
`CONTENT_*`, `TEACHER_*`) с фильтром по действию и временем МСК;
|
||||||
|
«Показать ещё» переходит на следующую страницу (`page`/`limit`);
|
||||||
преподаватель/студент получают `403`.
|
преподаватель/студент получают `403`.
|
||||||
|
- [ ] **Сессия:** после входа cookie `LMS_SESSION` живёт 7 дней; при работе
|
||||||
|
дольше 15 минут страница продолжает работать без повторного входа
|
||||||
|
(access обновляется автоматически). Выход и смена пароля требуют повторного
|
||||||
|
входа; вход отчисленного/архивного студента и refresh закрытой группы
|
||||||
|
отклоняются.
|
||||||
- [ ] Проверить логи `web`, `gateway`, `identity`, `academic`, `progress` на
|
- [ ] Проверить логи `web`, `gateway`, `identity`, `academic`, `progress` на
|
||||||
`ERROR`/`WARN` после smoke.
|
`ERROR`/`WARN` после smoke.
|
||||||
|
|
||||||
## 4. Известные пробелы и follow-up
|
## 4. Известные пробелы и follow-up
|
||||||
|
|
||||||
1. **Legacy-активы без привязки.** Видимость связанных активов реализована:
|
1. **Старый формат импорта не поддерживается.** Импорт принимает только
|
||||||
`V12` восстанавливает `article_assets` по markdown-ссылкам, а маршрут
|
шаблон v2 (поток/курс/номер/степень/год/ФИО/№/база). Ограничение
|
||||||
`/api/v1/content/assets/{name}` проверяет курс/аудиторию и публикацию.
|
зафиксировано в [11](11-stage1-report.md). Номер группы — числовой, поэтому
|
||||||
Остаточный случай — файлы, на которые нет ссылок ни в одной статье: они
|
отдельного требования к ведущим нулям нет.
|
||||||
остаются публичными. Нужна инвентаризация и привязка либо удаление таких
|
2. **Пагинация аудита — offset.** `GET /api/v1/audit` поддерживает `page` и
|
||||||
файлов; до этого fallback сохраняется, чтобы не ломать старые ссылки.
|
`limit` (максимум 500), UI — «Показать ещё». Для очень больших журналов
|
||||||
Детали — в [15](15-stage5-course-materials.md).
|
keyset-пагинация была бы устойчивее к вставкам между страницами.
|
||||||
2. **Старый формат импорта не поддерживается.** Импорт принимает только
|
3. **N+1 в отдельных списках.** Имена групп курса отдаются одним вызовом
|
||||||
шаблон v2 (поток/курс/номер/степень/год/ФИО/№/база). Ведущие нули номера
|
(`GET /api/v1/courses/{id}/groups`), но часть лабораторных списков и
|
||||||
группы нормализуются к числу (`01` → `1`), поэтому `ПИ-01БО` и `ПИ-1БО`
|
ведомости на больших объёмах всё ещё собирается по частям. Полный нагрузочный
|
||||||
неразличимы; для разных когорт нужен строковый номер. Ограничение
|
профиль 100 групп / 1000 студентов проходит, однако пакетные выборки для
|
||||||
зафиксировано в [11](11-stage1-report.md).
|
оставшихся экранов — лабораторная деталь, а не блокер передачи.
|
||||||
3. **JWT живёт до 30 дней.** Web-сессия перепроверяет статус каждые 45 секунд
|
4. **Нет абсолютного лимита web-сессии.** Сессия скользящая (7 дней) и
|
||||||
и закрывает UI при отчислении/архиве, но уже выданный JWT для прямого
|
отзывается при выходе, смене пароля, отчислении/архиве и закрытии группы,
|
||||||
вызова API действует до истечения. Нужны короткий TTL + refresh,
|
но при непрерывной активности может жить бессрочно. Если политика требует
|
||||||
denylist/revocation или проверка статуса на каждом API-вызове.
|
принудительного повторного входа раз в N дней, нужен абсолютный `max-age`
|
||||||
4. **Пагинация и N+1.** Журнал аудита ограничен `limit` (максимум 500), но
|
поверх sliding-сессии.
|
||||||
без offset/keyset; часть списков групп, материалов и ведомости отдаётся
|
|
||||||
целиком. На больших курсах нужны пагинация и пакетные выборки вместо
|
|
||||||
запросов в цикле.
|
|
||||||
|
|
||||||
## 5. Рекомендуемые следующие шаги
|
## 5. Рекомендуемые следующие шаги
|
||||||
|
|
||||||
Приоритет — сверху вниз:
|
Приоритет — сверху вниз:
|
||||||
|
|
||||||
1. **Инвентаризация legacy-активов.** Найти файлы без строк в `article_assets`,
|
1. **Инвентаризация legacy-активов.** Несвязанные файлы уже не публичны; админ
|
||||||
привязать их к статьям или удалить; после этого можно убрать публичный
|
получает их список через `GET /api/v1/content/legacy-assets`, после чего
|
||||||
fallback для несвязанных файлов.
|
привязывает к статьям или удаляет. Это гигиена хранилища, а не открытый
|
||||||
2. **JWT/refresh.** Короткий TTL access-токена + refresh, denylist/revocation
|
риск доступа.
|
||||||
или проверка статуса на каждом API-вызове; закрыть остаточный риск
|
2. **Keyset-пагинация и оставшиеся N+1.** Устойчивый курсор для аудита,
|
||||||
отчисленного/архивного пользователя с уже выданным токеном.
|
пакетные выборки для лабораторных списков и ведомости на больших курсах.
|
||||||
3. **Пагинация и устранение N+1.** Keyset/offset для аудита, групп, материалов
|
3. **Абсолютный лимит сессии.** Опционально: принудительный повторный вход
|
||||||
и ведомости; пакетные выборки профилей, комментариев и сдач.
|
раз в N дней поверх 7-дневной sliding-сессии.
|
||||||
4. **Внешняя интеграция «Контингент»/next.uniyar.ac.ru.** Отдельный
|
4. **Внешняя интеграция «Контингент»/next.uniyar.ac.ru.** Отдельный
|
||||||
anti-corruption слой, идемпотентная синхронизация, журнал и отчёт дрифта;
|
anti-corruption слой, идемпотентная синхронизация, журнал и отчёт дрифта;
|
||||||
внешние идентификаторы добавляются миграцией.
|
внешние идентификаторы добавляются миграцией.
|
||||||
@@ -288,17 +313,22 @@ negative-тестами «свой/чужой курс».
|
|||||||
|
|
||||||
Этап 6 закрыт: права преподавателя на запись, аудит, CI, security-матрица,
|
Этап 6 закрыт: права преподавателя на запись, аудит, CI, security-матрица,
|
||||||
upgrade-тест миграций, видимость активов и deploy-runbook проверены.
|
upgrade-тест миграций, видимость активов и deploy-runbook проверены.
|
||||||
Остаточные риски (legacy-активы, 30-дневный JWT, ведущие нули номера группы,
|
Продуктовые риски закрыты: legacy-активы secure-by-default, access/refresh и
|
||||||
пагинация/N+1) задокументированы в разделе 4 и не блокируют передачу.
|
web-сессия с отзывом, пагинация аудита и bulk-выдача групп. Оставшиеся
|
||||||
|
ограничения (offset-пагинация, отдельные N+1, отсутствие абсолютного лимита
|
||||||
|
сессии) задокументированы в разделе 4 и не блокируют передачу.
|
||||||
|
|
||||||
## 7. LDD baseline
|
## 7. LDD baseline
|
||||||
|
|
||||||
После этапа 6 текущая функциональность проверена через LDD-контур:
|
После этапа 6 и закрытия рисков текущая функциональность проверена через
|
||||||
реестр `docs/evidence/claims.json` (37 claim, 20 P0 / 17 P1), gate
|
LDD-контур: реестр `docs/evidence/claims.json` (**46 claims, 25 P0 / 21 P1**),
|
||||||
`./gradlew evidenceCheck`, upgrade-скрипт, runtime-smoke, mutation testing
|
gate `./gradlew evidenceCheck`, upgrade-скрипт, runtime-smoke, smoke-job,
|
||||||
и браузерный Playwright E2E. Результат: **37/37 claims proven, P0 gaps = 0**,
|
mutation testing и браузерный Playwright E2E. Результат:
|
||||||
276 тестов; mutation strength `CourseAccessService` 75%,
|
**46/46 claims proven, P0 gaps = 0, 319 тестов, 0 failures**; mutation strength
|
||||||
`PromotionPlanner` 69%; browser E2E 4/4; конкурентные apply/rollback и
|
`CourseAccessService` 75%, `PromotionPlanner` 69%; browser E2E 4/4;
|
||||||
нагрузочный smoke (p95 0.868 c) зелёные. E2E нашёл и закрыл реальный дефект
|
конкурентные apply/rollback и масштабный нагрузочный smoke (100 групп /
|
||||||
|
1000 студентов, 125 параллельных чтений, 0 ошибок, p95 < 1 с) зелёные.
|
||||||
|
Smoke-job выполняется на каждом push/MR, полный `release-evidence` обязателен
|
||||||
|
для тегов и доступен вручную для веток. E2E нашёл и закрыл реальный дефект
|
||||||
Jackson 3/Kotlin defaults. Подробности и остаточные ограничения —
|
Jackson 3/Kotlin defaults. Подробности и остаточные ограничения —
|
||||||
[`docs/evidence/baseline-2026-10-27.md`](evidence/baseline-2026-10-27.md).
|
[`docs/evidence/baseline-2026-10-27.md`](evidence/baseline-2026-10-27.md).
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid).
|
|||||||
| [12-stage2-report.md](12-stage2-report.md) | Отчёт этапа 2: курсы, bootstrap курса C, привязка и копирование лабораторных, миграция V10 |
|
| [12-stage2-report.md](12-stage2-report.md) | Отчёт этапа 2: курсы, bootstrap курса C, привязка и копирование лабораторных, миграция V10 |
|
||||||
| [13-stage3-report.md](13-stage3-report.md) | Отчёт этапа 3: преподаватели, доступ к курсам, внутренние API, кабинет и профиль студента курса |
|
| [13-stage3-report.md](13-stage3-report.md) | Отчёт этапа 3: преподаватели, доступ к курсам, внутренние API, кабинет и профиль студента курса |
|
||||||
| [14-stage4-report.md](14-stage4-report.md) | Отчёт этапа 4: повышение, академотпуск, отчисление, архив, восстановление и удаление |
|
| [14-stage4-report.md](14-stage4-report.md) | Отчёт этапа 4: повышение, академотпуск, отчисление, архив, восстановление и удаление |
|
||||||
| [15-stage5-course-materials.md](15-stage5-course-materials.md) | Отчёт этапа 5: курсовые материалы, публичные `ALL`-главы, видимость активов и legacy-файлы без привязки |
|
| [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-матрица, чек-лист деплоя и остаточные риски |
|
| [16-stage6-report.md](16-stage6-report.md) | Отчёт этапа 6: права преподавателя на запись, аудит, CI, upgrade-тест миграций, security-матрица, чек-лист деплоя и остаточные ограничения |
|
||||||
| [evidence/README.md](evidence/README.md) | LDD: реестр утверждений, evidence gate, runtime- и migration-проверки |
|
| [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, миграции, доступ, статусы, повышение |
|
| [ADR 0001](adr/0001-groups-courses-foundation.md) | Ключевые архитектурные решения: курс C, миграции, доступ, статусы, повышение |
|
||||||
|
|
||||||
## Кратко о системе
|
## Кратко о системе
|
||||||
|
|||||||
@@ -12,8 +12,9 @@
|
|||||||
`./gradlew evidenceCheck`.
|
`./gradlew evidenceCheck`.
|
||||||
- **P1** — важное поведение/UX/контракт. Пробел показывается в отчёте, но не
|
- **P1** — важное поведение/UX/контракт. Пробел показывается в отчёте, но не
|
||||||
блокирует сборку.
|
блокирует сборку.
|
||||||
- **runtime** — проверка на реально поднятом стеке. Выполняется отдельным
|
- **runtime** — проверка на реально поднятом стеке. Быстрый smoke
|
||||||
скриптом; в unit-CI может отсутствовать, но обязательна перед релизом.
|
(`scripts/ldd-smoke.sh`) выполняется в CI на каждом push/MR; полный
|
||||||
|
runtime/scale — в `release-evidence` и обязателен перед релизом.
|
||||||
|
|
||||||
## Реестр
|
## Реестр
|
||||||
|
|
||||||
@@ -39,19 +40,22 @@
|
|||||||
# 2. Гейт свидетельств (P0 без evidence = fail)
|
# 2. Гейт свидетельств (P0 без evidence = fail)
|
||||||
./gradlew evidenceCheck
|
./gradlew evidenceCheck
|
||||||
|
|
||||||
# 3. Полный runtime-smoke на одноразовой БД
|
# 3. Быстрый smoke полного стека (то же, что CI job smoke)
|
||||||
|
./scripts/ldd-smoke.sh
|
||||||
|
|
||||||
|
# 4. Полный runtime-smoke + scale load на одноразовой БД
|
||||||
./scripts/ldd-runtime.sh
|
./scripts/ldd-runtime.sh
|
||||||
|
|
||||||
# 4. Проверка upgrade с master-схемы
|
# 5. Проверка upgrade с master-схемы
|
||||||
./scripts/ldd-migration-upgrade.sh
|
./scripts/ldd-migration-upgrade.sh
|
||||||
|
|
||||||
# 5. Mutation testing критичных P0-компонентов
|
# 6. Mutation testing критичных P0-компонентов
|
||||||
./scripts/ldd-mutation.sh
|
./scripts/ldd-mutation.sh
|
||||||
|
|
||||||
# 6. Браузерный E2E (Playwright + Chromium)
|
# 7. Браузерный E2E (Playwright + Chromium)
|
||||||
./scripts/ldd-browser.sh
|
./scripts/ldd-browser.sh
|
||||||
|
|
||||||
# 7. Полный release gate
|
# 8. Полный release gate
|
||||||
./scripts/ldd-release.sh
|
./scripts/ldd-release.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -74,26 +78,36 @@ Platform 6 из Spring Boot 4.
|
|||||||
доказательства.
|
доказательства.
|
||||||
6. При переименовании теста обновляется `claims.json`; устаревшая ссылка —
|
6. При переименовании теста обновляется `claims.json`; устаревшая ссылка —
|
||||||
это gap, а не «забыли».
|
это gap, а не «забыли».
|
||||||
7. Runtime-свидетельства обязательны перед релизом; CI проверяет unit/
|
7. Runtime-свидетельства обязательны перед релизом; CI на каждом push/MR
|
||||||
integration-слой.
|
выполняет unit/integration-слой и быстрый smoke, полный release gate —
|
||||||
|
для тегов (вручную для веток).
|
||||||
|
|
||||||
## Текущий baseline
|
## Текущий baseline
|
||||||
|
|
||||||
Первый LDD-цикл выполнен 2026-10-27/28:
|
LDD-циклы выполнены 2026-10-27/28; последний — после закрытия продуктовых
|
||||||
|
рисков:
|
||||||
|
|
||||||
- **276 автоматических тестов**, 0 failures/errors;
|
- **319 автоматических тестов**, 0 failures/errors;
|
||||||
- **37 claims**: 20 P0 + 17 P1. P0 — доступы, повышение, жизненный цикл,
|
- **46 claims**: 25 P0 + 21 P1, **46/46 proven, P0 gaps = 0**. P0 — доступы,
|
||||||
профиль/скачивание, материалы/активы, миграция V13, аудит; P1 — группы,
|
повышение, жизненный цикл, профиль/скачивание, материалы/активы
|
||||||
авторство, UI, контракты, импорт, копирование лаб, runtime, mutation и
|
(включая secure-by-default legacy-файлы), миграция V13, аудит, refresh-токены
|
||||||
browser E2E;
|
и их отзыв; P1 — группы, авторство, UI, контракты, импорт, копирование лаб,
|
||||||
|
пагинация аудита, bulk-группы курса, runtime, mutation и browser E2E;
|
||||||
- mutation testing: `CourseAccessService` 75% test strength,
|
- mutation testing: `CourseAccessService` 75% test strength,
|
||||||
`PromotionPlanner` 69%;
|
`PromotionPlanner` 69%;
|
||||||
- Playwright: 4 сценария (admin, teacher, student/anonymous, сдача+mobile), все зелёные;
|
- Playwright: 4 сценария (admin, teacher, student/anonymous, сдача+mobile),
|
||||||
- runtime-smoke, master-upgrade, mutation и browser E2E вынесены в отдельные
|
все зелёные;
|
||||||
скрипты.
|
- масштабный 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 не применял
|
В ходе E2E найден и исправлен реальный дефект: Jackson 3 не применял
|
||||||
Kotlin-модуль, поэтому DTO с default-значениями требовали все поля JSON
|
Kotlin-модуль, поэтому DTO с default-значениями требовали все поля JSON
|
||||||
(например, создание студента без `firstName`/`basis` давало 400).
|
(например, создание студента без `firstName`/`basis` давало 400).
|
||||||
|
|
||||||
Результаты и остаточные ограничения — в `baseline-2026-10-27.md`.
|
Остаточные ограничения: offset-пагинация аудита, N+1 в отдельных лабораторных
|
||||||
|
списках/ведомости, отсутствие абсолютного лимита web-сессии. Legacy-активы,
|
||||||
|
срок жизни JWT и «ведущие нули» больше не являются открытыми рисками.
|
||||||
|
Результаты — в `baseline-2026-10-27.md`.
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
# LDD baseline: 2026-10-27/28
|
# LDD baseline: 2026-10-27/28
|
||||||
|
|
||||||
Первый и второй LDD-циклы проверки текущей функциональности LMS.
|
Три LDD-цикла проверки текущей функциональности LMS; последний выполнен
|
||||||
Добавлены реестр claims, gate `evidenceCheck`, runtime/migration/mutation
|
2026-10-28 после закрытия продуктовых рисков. Добавлены реестр claims, gate
|
||||||
скрипты и браузерный E2E на Playwright. В ходе проверки найден и исправлен
|
`evidenceCheck`, runtime/migration/mutation/smoke-скрипты и браузерный E2E на
|
||||||
реальный дефект Jackson 3.
|
Playwright. В ходе проверки найден и исправлен реальный дефект Jackson 3.
|
||||||
|
|
||||||
## 1. Что проверялось
|
## 1. Что проверялось
|
||||||
|
|
||||||
Реестр: `docs/evidence/claims.json`, **37 утверждений**.
|
Реестр: `docs/evidence/claims.json`, **44 утверждения**.
|
||||||
|
|
||||||
| Критичность | Claims | Proven | Gaps |
|
| Критичность | Claims | Proven | Gaps |
|
||||||
|---|---:|---:|---:|
|
|---|---:|---:|---:|
|
||||||
| P0 | 20 | 20 | 0 |
|
| P0 | 25 | 25 | 0 |
|
||||||
| P1 | 17 | 17 | 0 |
|
| P1 | 21 | 21 | 0 |
|
||||||
| **Итого** | **37** | **37** | **0** |
|
| **Итого** | **46** | **46** | **0** |
|
||||||
|
|
||||||
Домены:
|
Домены:
|
||||||
|
|
||||||
@@ -22,12 +22,14 @@
|
|||||||
- повышение: dry-run, apply, идемпотентность, rollback, cancel;
|
- повышение: dry-run, apply, идемпотентность, rollback, cancel;
|
||||||
- жизненный цикл: академ, возврат, отчисление, архив, восстановление, вход;
|
- жизненный цикл: академ, возврат, отчисление, архив, восстановление, вход;
|
||||||
- профиль студента и скачивание сдач;
|
- профиль студента и скачивание сдач;
|
||||||
- материалы и активы;
|
- материалы и активы, включая secure-by-default legacy-файлы;
|
||||||
- миграция V13 и полный upgrade с master-схемы;
|
- миграции V11 и V13, полный upgrade с master-схемы;
|
||||||
- аудит: токен, admin-only, сохранность после удаления, representative-хуки;
|
- аудит: токен, admin-only, сохранность после удаления, representative-хуки,
|
||||||
- группы курса, авторство материалов, ведомость, UI/контракты, импорт,
|
пагинация;
|
||||||
|
- аутентификация: короткий access-токен, ротация/отзыв refresh, web-сессия;
|
||||||
|
- группы курса без N+1, авторство материалов, ведомость, UI/контракты, импорт,
|
||||||
копирование лабораторных;
|
копирование лабораторных;
|
||||||
- runtime-smoke, mutation testing, браузерный E2E.
|
- runtime-smoke, масштабный load, mutation testing, браузерный E2E и CI smoke.
|
||||||
|
|
||||||
## 2. Команды и результаты
|
## 2. Команды и результаты
|
||||||
|
|
||||||
@@ -37,7 +39,7 @@
|
|||||||
./gradlew test --rerun-tasks
|
./gradlew test --rerun-tasks
|
||||||
```
|
```
|
||||||
|
|
||||||
Результат: **276 тестов, 0 failures, 0 errors, 0 skipped**.
|
Результат: **319 тестов, 0 failures, 0 errors, 0 skipped**.
|
||||||
`ktlintCheck` и `detekt` — зелёные.
|
`ktlintCheck` и `detekt` — зелёные.
|
||||||
|
|
||||||
### 2.2. LDD gate
|
### 2.2. LDD gate
|
||||||
@@ -46,7 +48,7 @@
|
|||||||
./gradlew evidenceCheck
|
./gradlew evidenceCheck
|
||||||
```
|
```
|
||||||
|
|
||||||
Результат: **37/37 claims proven, P0 gaps = 0**.
|
Результат: **46/46 claims proven, P0 gaps = 0**.
|
||||||
Отчёт: `build/reports/evidence/index.md`.
|
Отчёт: `build/reports/evidence/index.md`.
|
||||||
|
|
||||||
Gate устроен так:
|
Gate устроен так:
|
||||||
@@ -65,11 +67,11 @@ Gate устроен так:
|
|||||||
|
|
||||||
Сценарий: одноразовая PostgreSQL 18, master identity `V1–V3`, academic
|
Сценарий: одноразовая PostgreSQL 18, master identity `V1–V3`, academic
|
||||||
`V1–V5`, progress `V1–V9,V11,V12`, синтетические данные, затем ветка:
|
`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` сохранились;
|
- `students`, `groups`, `membership_history` сохранились;
|
||||||
- `labs.course_id`, `chapters.course_id`, `article_assets` созданы;
|
- `labs.course_id`, `chapters.course_id`, `article_assets` созданы;
|
||||||
- `grade_comments.author_account_id`, `lab_grades.updated_by_account_id` созданы;
|
- `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`.
|
Результат: **PASS**, evidence: `build/evidence/migration.json`.
|
||||||
|
|
||||||
### 2.4. Runtime-smoke полного стека
|
### 2.4. Runtime-smoke полного стека и CI smoke
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/ldd-runtime.sh
|
./scripts/ldd-runtime.sh
|
||||||
|
./scripts/ldd-smoke.sh # то же, что CI job smoke
|
||||||
```
|
```
|
||||||
|
|
||||||
Сценарий: одноразовая БД, `bootJar`, запуск identity/academic/progress/gateway/web,
|
Сценарий: одноразовая БД, `bootJar`, запуск identity/academic/progress/gateway/web,
|
||||||
@@ -99,9 +102,68 @@ identity `V4–V10`, academic `V6–V12`, progress `V13`.
|
|||||||
- web `/login` отдаёт `200`, публичное дерево материалов доступно, курсовые
|
- web `/login` отдаёт `200`, публичное дерево материалов доступно, курсовые
|
||||||
материалы анонимно — `401`.
|
материалы анонимно — `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
|
```bash
|
||||||
./scripts/ldd-mutation.sh
|
./scripts/ldd-mutation.sh
|
||||||
@@ -119,7 +181,7 @@ PIT запускается через CLI с JUnit 4 harness: JUnit5-плаги
|
|||||||
Остаток — эквивалентные мутации, Kotlin-интринсики и ветки, не входящие в
|
Остаток — эквивалентные мутации, Kotlin-интринсики и ветки, не входящие в
|
||||||
текущий P0-контур.
|
текущий P0-контур.
|
||||||
|
|
||||||
### 2.6. Браузерный E2E
|
### 2.9. Браузерный E2E
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/ldd-browser.sh
|
./scripts/ldd-browser.sh
|
||||||
@@ -136,25 +198,27 @@ Playwright/Chromium на полном стеке, одноразовая БД:
|
|||||||
Результат: **4/4 passed**, evidence: `build/evidence/browser.json`.
|
Результат: **4/4 passed**, evidence: `build/evidence/browser.json`.
|
||||||
Дополнительно проверены сдача файла студентом и mobile-размер teacher-страницы.
|
Дополнительно проверены сдача файла студентом и mobile-размер teacher-страницы.
|
||||||
|
|
||||||
### 2.7. Конкурентные apply/rollback
|
### 2.10. Конкурентные apply/rollback
|
||||||
|
|
||||||
Добавлен `PromotionConcurrencyIntegrationTest` (без `@Transactional`, два потока,
|
`PromotionConcurrencyIntegrationTest` (без `@Transactional`, два потока,
|
||||||
advisory lock): два одновременных apply идемпотентны и пишут history один раз;
|
advisory lock): два одновременных apply идемпотентны и пишут history один раз;
|
||||||
два одновременных rollback дают ровно один успех и один `ConflictException`,
|
два одновременных rollback дают ровно один успех и один `ConflictException`,
|
||||||
состояние восстанавливается один раз.
|
состояние восстанавливается один раз.
|
||||||
|
|
||||||
### 2.8. Нагрузочный smoke
|
### 2.11. Release gate и CI
|
||||||
|
|
||||||
`ldd-runtime.sh` выполняет 100 чтений (10 workers) курса, лаб, профиля и аудита:
|
|
||||||
**0 ошибок, avg 0.141 c, p95 0.868 c** (порог p95 < 2 c).
|
|
||||||
|
|
||||||
### 2.9. Release gate
|
|
||||||
|
|
||||||
`./scripts/ldd-release.sh` последовательно запускает тесты/линтеры, upgrade,
|
`./scripts/ldd-release.sh` последовательно запускает тесты/линтеры, upgrade,
|
||||||
mutation, runtime и browser E2E, затем финальный `evidenceCheck`.
|
mutation, runtime + scale и browser E2E, затем финальный `evidenceCheck`.
|
||||||
В `.gitlab-ci.yml` добавлен ручной job `release-evidence` с Chromium/Node.
|
|
||||||
|
|
||||||
### 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`, потому что
|
Первый browser E2E упал на создании студента: API вернул `400`, потому что
|
||||||
Jackson 3 не применял Kotlin-модуль, и DTO с default-значениями требовали все
|
Jackson 3 не применял Kotlin-модуль, и DTO с default-значениями требовали все
|
||||||
@@ -166,14 +230,21 @@ optional json fields`.
|
|||||||
## 3. Что это доказывает
|
## 3. Что это доказывает
|
||||||
|
|
||||||
- Критичные P0-инварианты доступа, повышения, жизненного цикла, профиля,
|
- Критичные P0-инварианты доступа, повышения, жизненного цикла, профиля,
|
||||||
материалов, миграции и аудита имеют зелёные автоматические свидетельства.
|
материалов, миграции, аудита и аутентификации имеют зелёные автоматические
|
||||||
|
свидетельства.
|
||||||
- Приложение собирается, мигрирует с master-схемы и поднимается полным
|
- Приложение собирается, мигрирует с master-схемы и поднимается полным
|
||||||
стеком на чистой БД.
|
стеком на чистой БД.
|
||||||
- Ключевой доменный поток «админ → группа → курс → преподаватель → лаба»
|
- Ключевой доменный поток «админ → группа → курс → преподаватель → лаба»
|
||||||
проходит через реальные HTTP/gateway/БД и браузер.
|
проходит через реальные HTTP/gateway/БД и браузер.
|
||||||
- Чужой преподаватель не получает доступ к курсу/лабе/аудиту на реальном стеке.
|
- Чужой преподаватель не получает доступ к курсу/лабе/аудиту на реальном стеке.
|
||||||
|
- Legacy-актив без привязки не утекает обычным пользователям, а админ может
|
||||||
|
провести инвентаризацию.
|
||||||
|
- Refresh-токены ротируются и отзываются; закрытый статус блокирует продление
|
||||||
|
сессии, а web-сессия переживает 15-минутный access-токен.
|
||||||
|
- Аудит листается по страницам, а группы курса отдаются одним вызовом без N+1.
|
||||||
- Аудит пишется и читается; upgrade не теряет синтетические данные.
|
- Аудит пишется и читается; upgrade не теряет синтетические данные.
|
||||||
- Мутационная прочность критичных компонентов измерена, а не заявлена.
|
- Мутационная прочность критичных компонентов измерена, а не заявлена.
|
||||||
|
- Масштабный load на 100 группах / 1000 студентов проходит без ошибок.
|
||||||
- Реальный дефект DTO/Jackson найден именно E2E-контуром и закрыт тестом.
|
- Реальный дефект DTO/Jackson найден именно E2E-контуром и закрыт тестом.
|
||||||
|
|
||||||
## 4. Чего это не доказывает
|
## 4. Чего это не доказывает
|
||||||
@@ -186,24 +257,32 @@ optional json fields`.
|
|||||||
3. **Браузерный E2E — один Chromium и 4 сценария.** Нет визуальной регрессии,
|
3. **Браузерный E2E — один Chromium и 4 сценария.** Нет визуальной регрессии,
|
||||||
Safari/Firefox и полного мобильного набора страниц.
|
Safari/Firefox и полного мобильного набора страниц.
|
||||||
4. **Нагрузочный smoke — не benchmark.** Одна машина, синтетические данные,
|
4. **Нагрузочный smoke — не benchmark.** Одна машина, синтетические данные,
|
||||||
без SLA production.
|
без SLA production; 125 чтений и p95 < 1 с — ориентир, а не гарантия.
|
||||||
5. **Legacy-активы без `article_assets` остаются публичными.**
|
5. **Offset-пагинация аудита.** Вставки между страницами могут сдвигать
|
||||||
6. **JWT живёт до 30 дней.** Web перепроверяет статус, прямой API — нет.
|
выдачу; keyset-курсор не реализован.
|
||||||
7. **Ведущие нули номера группы нормализуются** (`01` → `1`).
|
6. **N+1 в отдельных лабораторных списках/ведомости.** Имена групп курса и
|
||||||
8. **Нет пагинации/keyset** для части списков и N+1 в отдельных экранах.
|
аудит исправлены, но не все большие списки собраны пакетно.
|
||||||
9. **Runtime/mutation/browser E2E не входят в unit-CI**; это release-скрипты.
|
7. **Нет абсолютного лимита web-сессии.** Sliding-сессия может продлеваться
|
||||||
|
бесконечно при непрерывной активности; отзыв по статусу при этом работает.
|
||||||
|
8. **Полный release-evidence не на каждом коммите.** На push/MR идёт smoke,
|
||||||
|
а mutation/browser/scale — для тегов или вручную.
|
||||||
|
9. **Внешняя интеграция «Контингент» не проверялась** — она остаётся заделом.
|
||||||
|
10. **Номер группы числовой.** Отдельного требования к ведущим нулям нет;
|
||||||
|
`01` и `1` — одно значение, это не открытый риск.
|
||||||
|
|
||||||
## 5. Вывод
|
## 5. Вывод
|
||||||
|
|
||||||
Текущая функциональность прошла LDD-контур: **37/37 claims proven, P0 gaps = 0**,
|
Текущая функциональность прошла LDD-контур: **46/46 claims proven, P0 gaps = 0**,
|
||||||
273 теста, upgrade, mutation baseline и браузерный E2E зелёные. Это
|
**319 тестов**, upgrade, mutation baseline, browser E2E, smoke и масштабный load
|
||||||
достаточное основание продолжать доработки, но не «формальное доказательство
|
зелёные. Это достаточное основание продолжать доработки, но не «формальное
|
||||||
всей системы».
|
доказательство всей системы».
|
||||||
|
|
||||||
Следующий цикл LDD:
|
Следующий цикл LDD:
|
||||||
|
|
||||||
1. расширить browser E2E на публикацию оценки и остальные мобильные страницы;
|
1. почистить инвентарь legacy-активов через `GET /api/v1/content/legacy-assets`
|
||||||
2. добавить нагрузочный профиль с реалистичным объёмом групп/студентов;
|
(файлы уже не публичны, это гигиена хранилища);
|
||||||
3. закрыть известные продуктовые риски: legacy-активы, JWT 30 дней, ведущие
|
2. добавить keyset-пагинацию аудита и убрать оставшийся N+1 в лабораторных
|
||||||
нули, пагинация/N+1;
|
списках/ведомости;
|
||||||
4. сделать release-evidence job обязательным для тегов.
|
3. при необходимости ввести абсолютный лимит web-сессии;
|
||||||
|
4. расширить browser E2E на публикацию оценки и остальные мобильные страницы;
|
||||||
|
5. подготовить внешнюю интеграцию «Контингент»/`next.uniyar.ac.ru`.
|
||||||
|
|||||||
@@ -232,14 +232,14 @@
|
|||||||
"criticality": "P0",
|
"criticality": "P0",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"requirement": "docs/07-groups-courses.md, §6",
|
"requirement": "docs/07-groups-courses.md, §6",
|
||||||
"claim": "Скрытый курсовой актив не отдаётся студенту, но доступен преподавателю; чужой курс получает 403; legacy без связи публичен.",
|
"claim": "Скрытый курсовой актив не отдаётся студенту, но доступен преподавателю; чужой курс получает 403; публичный глобальный ALL-актив доступен без входа.",
|
||||||
"oracle": "HTTP-статус и содержимое файла",
|
"oracle": "HTTP-статус и содержимое файла",
|
||||||
"evidence": [
|
"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#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#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",
|
"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#parses new template and splits full name()",
|
||||||
"ru.ysu.lms.identity.roster.StudentImportParserTest#reports duplicate student number()"
|
"ru.ysu.lms.identity.roster.StudentImportParserTest#reports duplicate student number()"
|
||||||
],
|
],
|
||||||
"limitations": "Старый формат не поддерживается; ведущие нули нормализуются."
|
"limitations": "Старый формат импорта не поддерживается; номер группы числовой, отдельного требования к ведущим нулям нет."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "LDD-LAB-002",
|
"id": "LDD-LAB-002",
|
||||||
@@ -435,7 +435,7 @@
|
|||||||
"criticality": "P1",
|
"criticality": "P1",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"requirement": "docs/16-stage6-report.md, §2.2",
|
"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-наличие таблиц/колонок/индексов",
|
"oracle": "Успешное применение и postflight-наличие таблиц/колонок/индексов",
|
||||||
"runtime": [
|
"runtime": [
|
||||||
"migration.master_upgrade"
|
"migration.master_upgrade"
|
||||||
@@ -538,6 +538,137 @@
|
|||||||
"ru.ysu.lms.identity.promotion.PromotionApplyRollbackPropertyTest#generated cohorts apply to plan and rollback to before-state()"
|
"ru.ysu.lms.identity.promotion.PromotionApplyRollbackPropertyTest#generated cohorts apply to plan and rollback to before-state()"
|
||||||
],
|
],
|
||||||
"limitations": "10 детерминированных сценариев в одной транзакции; не заменяет property-генератор по всем полям."
|
"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."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user