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

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