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

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

View File

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