исправление багов
This commit is contained in:
@@ -82,7 +82,7 @@ sequenceDiagram
|
||||
| `TenantDataSourceConfig` | Загружает конфигурацию тенантов из JSON-файла, создаёт HikariCP пулы |
|
||||
| `TenantWebMvcConfig` | Регистрирует `TenantInterceptor` и `AuthorizationInterceptor` в MVC-слое |
|
||||
| `TenantConfigWatcher` | Периодически (каждые 30 сек) перечитывает `tenants.json`, синхронизирует тенантов |
|
||||
| `ConfigMapUpdater` | Обновляет Kubernetes ConfigMap при добавлении/удалении тенанта через API |
|
||||
| `KubernetesTenantSecretUpdater` | Обновляет Kubernetes Secret с tenant-конфигурацией через API, проверяя TLS по service-account CA |
|
||||
| `TenantConfig` | POJO с параметрами тенанта: `name`, `domain`, `url`, `username`, `password` |
|
||||
|
||||
### Определение тенанта
|
||||
@@ -101,7 +101,10 @@ sequenceDiagram
|
||||
|
||||
Список тенантов хранится в JSON-файле:
|
||||
- **Локально:** `backend/tenants.json` (не коммитится; шаблон — `backend/tenants.example.json`)
|
||||
- **Продакшн:** Kubernetes ConfigMap `tenants-config`, монтируется в `/config/tenants.json`
|
||||
- **Продакшн:** внешний Kubernetes Secret `tenants-secret`, ключ `tenants.json` монтируется в `/config/tenants.json`
|
||||
|
||||
`tenants-secret` не создаётся отслеживаемыми production-манифестами. Его подготавливает
|
||||
оператор через secret manager по [`SECURITY_RUNBOOK.md`](SECURITY_RUNBOOK.md).
|
||||
|
||||
Формат:
|
||||
```json
|
||||
@@ -118,9 +121,14 @@ sequenceDiagram
|
||||
|
||||
### Жизненный цикл тенанта
|
||||
|
||||
1. **Добавление через API:** `POST /api/database/tenants` → создаёт HikariCP пул → запускает Flyway миграции → обновляет ConfigMap
|
||||
2. **Синхронизация подов:** `TenantConfigWatcher` каждые 30 сек проверяет `tenants.json` → добавляет новые / удаляет отсутствующие тенанты
|
||||
3. **Удаление:** `DELETE /api/database/tenants/{domain}` → закрывает пул → обновляет ConfigMap
|
||||
1. **Добавление через API:** `POST /api/database/tenants` → создаёт HikariCP пул → запускает Flyway миграции → обновляет `tenants-secret`
|
||||
2. **Синхронизация подов:** `TenantConfigWatcher` каждые 30 сек проверяет смонтированный `tenants.json` → добавляет новые / удаляет отсутствующие тенанты
|
||||
3. **Удаление:** `DELETE /api/database/tenants/{domain}` → закрывает пул → обновляет `tenants-secret`
|
||||
|
||||
Для обращения к Kubernetes API backend загружает
|
||||
`/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`, использует стандартную PKIX-проверку
|
||||
цепочки и обязательную проверку hostname. Trust-all fallback отсутствует: ошибка CA или TLS
|
||||
завершает персистенцию безопасным отказом.
|
||||
|
||||
`TenantConfigWatcher` сравнивает содержимое `tenants.json` по SHA-256, а не по `String.hashCode()`, чтобы изменение ConfigMap не пропускалось из-за 32-битной коллизии.
|
||||
|
||||
@@ -134,6 +142,47 @@ sequenceDiagram
|
||||
|
||||
---
|
||||
|
||||
## Транзакционные изменения расписания
|
||||
|
||||
`ScheduleRuleAdminController` является тонким HTTP-адаптером. Чтение, создание, изменение
|
||||
и архивация правил выполняются через `ScheduleRuleService`; публичные write-методы сервиса
|
||||
образуют транзакционные границы, поэтому ошибки валидации и конфликты выходят за Spring
|
||||
proxy и приводят к rollback.
|
||||
|
||||
Создание правила до остальных запросов к БД захватывает `PESSIMISTIC_WRITE` на строке
|
||||
целевого семестра. Update сначала блокирует строку правила, затем старый и новый семестры в
|
||||
порядке ID; архивация блокирует правило и его семестр. После блокировок сервис заново
|
||||
проверяет слоты payload и активные правила семестра, поэтому конкурентные запросы разных
|
||||
pod не сохраняют два конфликтующих правила после проверки одного снимка.
|
||||
|
||||
`AcademicCalendarController` делегирует полную замену дневной сетки
|
||||
`AcademicCalendarGridService`. Публичный `replaceGrid()` проходит через транзакционный
|
||||
Spring proxy: весь payload и все activity types проверяются до bulk delete, затем выполняются
|
||||
`delete → flush → saveAll → flush`. Исключение не перехватывается внутри сервиса и вызывает
|
||||
rollback. Инвалидация кэша зарегистрирована через transaction synchronization и выполняется
|
||||
только после успешного commit.
|
||||
|
||||
`ScheduleOverrideController` является HTTP-адаптером, а create/update/delete выполняет
|
||||
`ScheduleOverrideService` через вызываемые Spring proxy-методы с `@Transactional`.
|
||||
Сервис строит базовый день через `ScheduleQueryService`, накладывает сохранённые overrides и
|
||||
кандидат общей функцией и только затем проверяет результирующие интервалы и ресурсы.
|
||||
|
||||
До чтения снимка даты сервис захватывает `pg_advisory_xact_lock` с отдельным namespace и
|
||||
точным `LocalDate.toEpochDay()`. Блокировка живёт до commit/rollback, работает между pod и
|
||||
изолирована по tenant-БД. При `PUT` и `DELETE` дополнительно блокируется ID override;
|
||||
при смене даты старая и новая даты захватываются в стабильном порядке. После блокировок
|
||||
строка перечитывается с `PESSIMISTIC_WRITE`, поэтому параллельное изменение не приводит к
|
||||
lost update или проверке устаревшего снимка.
|
||||
|
||||
Teacher-only чтение строит базовое расписание напрямую через
|
||||
`ScheduleGeneratorService.buildScheduleForTeacher()`. `ScheduleQueryService` дополнительно
|
||||
находит overrides с `newTeacher`, группирует их по дате, один раз строит базовый день каждой
|
||||
релевантной даты и добавляет только целевые `baseRuleSlotId`. Все overrides применяются до
|
||||
финального teacher-фильтра и дедупликации; без релевантных замен полный обход групп не
|
||||
выполняется.
|
||||
|
||||
---
|
||||
|
||||
## Аутентификация
|
||||
|
||||
Система использует access JWT и отзывные refresh-токены без включения полноценного Spring Security flow:
|
||||
@@ -145,6 +194,23 @@ sequenceDiagram
|
||||
5. При истечении access JWT клиент вызывает `POST /api/auth/refresh`; refresh-токен ротируется, старый хэш отзывается.
|
||||
6. `POST /api/auth/logout` отзывает текущий refresh-токен и очищает cookie.
|
||||
|
||||
Ротация refresh-токена имеет single-use семантику. `RefreshTokenService.rotate()` выполняется
|
||||
в транзакции, а `AuthRefreshTokenRepository` захватывает исходную строку через
|
||||
`PESSIMISTIC_WRITE`. Поэтому два одновременных запроса с одним cookie сериализуются:
|
||||
только первый создаёт следующий refresh-токен, второй видит уже отозванную строку и
|
||||
завершается без выпуска новой сессии.
|
||||
|
||||
Access JWT содержит claim'ы `tenant`, `userId`, `username`, `role`, `departmentId`, `iat`, `exp`, `jti`. `AuthorizationInterceptor` проверяет подпись, срок действия и совпадение `tenant` с `TenantContext`, затем применяет `@RequireRoles`. Это означает, что UI-роль в `localStorage` остаётся только удобством: backend возвращает `401`, если токен отсутствует/некорректен, и `403`, если роли недостаточно.
|
||||
|
||||
`AuthContext` существует только в границах одного servlet-запроса. Интерцептор очищает
|
||||
`ThreadLocal` до любых ранних выходов, устанавливает пользователя только после успешной
|
||||
проверки роли и повторно очищает контекст в `afterCompletion`. Поэтому отказ с `401`/`403`
|
||||
или публичный endpoint не может получить пользователя от предыдущего запроса того же
|
||||
потока контейнера.
|
||||
|
||||
`TenantInterceptor` по-прежнему отвечает за выбор БД тенанта по домену. Проверка авторизации выполняется отдельным интерцептором после tenant-resolution, поэтому токен, выданный на одном домене, не принимается на другом tenant-домене.
|
||||
|
||||
JWT-секрет не имеет встроенного значения и обязателен во всех окружениях. При профиле
|
||||
`prod` или `production` startup-проверка дополнительно отклоняет известные legacy/placeholder
|
||||
значения и требует `JWT_REFRESH_COOKIE_SECURE=true`. Production deployment явно включает
|
||||
профиль `production` и получает `JWT_SECRET` только через `secretKeyRef`.
|
||||
|
||||
Reference in New Issue
Block a user