Технический долг Среза 1: dev-seed под DEBUG, /docs и CORS в проде, / и 405, аудит входа, дата по МСК, seed не возвращает снятые права #10

Merged
aep merged 13 commits from fix/9-tech-debt-slice-1 into main 2026-10-05 19:18:45 +03:00

13 Commits

Author SHA1 Message Date
Andrey Epifancev
8bc76c69a0 docs(openspec): спеки синхронизированы с кодом issue #9 (ревью R-4)
- reference-data-seed: матрица role_permission целиком — только в пустую
  таблицу, в заполненную — лишь пары ролей/прав, вставленных тем же запуском
  (сценарии: снятое право, новые роль и право, все связи сняты); `--dev` только
  при DEBUG=true (код 2 до обращения к БД).
- audit-trail: требование «События входа» (LOGIN/LOGIN_FAILED, раздел SESSION)
  — исключение из «той же транзакции» с обоснованием, обрезка полей, доверенные
  прокси; строки в таблице событий.
- authn-authz: ссылка на аудит входа; требование «Прод-конфигурация
  HTTP-поверхности» — /docs, /redoc, /openapi.json только при DEBUG, CORS по
  CORS_ORIGINS, 405 на неверный метод, 404 под /api/v1, GET / без редиректа.

`openspec validate --specs`: 9 passed, 0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:49:46 +04:00
Andrey Epifancev
7b28554817 docs: «нет заголовка Authorization или схема не Bearer» — не попытка входа (ревью R-7, #9)
Формулировка уточнена в докстрингах `services/session_audit.py` и
`api/dependencies/auth.py`, README «Безопасность dev-режима» и CLAUDE.md; тест
событий входа дополнен запросом со схемой Basic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:48:52 +04:00
Andrey Epifancev
e8d14edfbb fix: CORS при DEBUG — * без allow_credentials (ревью R-11, #9)
`*` вместе с credentials спецификация CORS запрещает (Starlette тогда отражает
Origin запроса); фронт передаёт токен заголовком Authorization, cookie не
нужны. README «Конфигурация» уточнён.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:48:15 +04:00
Andrey Epifancev
2c83005564 fix: seed выдаёт права новых ролей и прав в заполненную матрицу (ревью R-3, R-9, #9)
При непустой `role_permission` вставляются пары, у которых роль или право
вставлены этим же прогоном (`_upsert` возвращает вставленные ключи): их
администратор снять не мог. Прочие пары по-прежнему не трогаются — снятые
права не возвращаются. Убрана мёртвая ветка `unchanged` (R-9): при пустой
таблице совпадений быть не может, при заполненной сверка не нужна.

Тесты: новое право и новая роль во временном seed-каталоге → повторный seed
выдаёт только их пары, снятая связь не возвращается; администратор снял все
связи → матрица заливается заново. README, CLAUDE.md, комментарий
`role_permissions.yaml` и докстринг `seed.py` уточнены.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:47:42 +04:00
Andrey Epifancev
f47cbeedda fix: X-Forwarded-For только от доверенных прокси — настройка FORWARDED_ALLOW_IPS (ревью R-2, OQ-65, #9)
`ProxyHeadersMiddleware(trusted_hosts=settings.FORWARDED_ALLOW_IPS)` вместо
`["*"]`: по умолчанию `127.0.0.1`, список через запятую или `*`. Иначе любой
клиент подставлял свой адрес в `audit_event.ip_address` (в том числе в
LOGIN_FAILED). `.env.example`/`.env.test`, README «Конфигурация» и
«Безопасность dev-режима» дополнены; compose `docs/dev` не менялся.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:46:17 +04:00
Andrey Epifancev
ea119ffaa5 fix: события входа обрезают user_agent до 512 и description до 1000 символов (ревью R-1, #9)
`clip` в `services/session_audit.py` применяется в `record_login` (общая точка
LOGIN/LOGIN_FAILED): заголовок User-Agent и email из токена присылает клиент,
без ограничения запись в журнал была бы произвольной длины. Тесты: помощник,
запись `SessionAudit` без БД и длинный User-Agent при 401 через API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:45:29 +04:00
Andrey Epifancev
900fdc8ece fix: seed не возвращает снятые права — матрица только в пустую role_permission (#9 п.7)
`_apply_role_permissions` (`scripts/seed_lib/apply.py`) загружает матрицу из
`role_permissions.yaml` только при пустой `role_permission`; иначе шаг
пропускается целиком с причиной в отчёте (`TableStat.skipped_reason`,
«role_permission: пропущено — …»). Права, снятые администратором, не
возвращаются; `role` и `permission` по-прежнему upsert'ятся.

Тесты: повторный прогон — «пропущено», матрица та же; снятая связь после
повторного seed не возвращается. README «Администрирование» и раздел seed
переписаны под новое поведение.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:30:09 +04:00
Andrey Epifancev
b17176f97c fix: «сегодня» для effective_date — дата по Москве (#9 п.6, follow-up #8)
`_today()` в `services/legal_entities.py` берёт московскую дату
(`common/clock.py`: `moscow_date`, `MOSCOW_TZ` = Europe/Moscow; без базы
часовых поясов — фиксированный UTC+3, Москва без перехода на летнее время с
2014 г.). Тесты на границе суток: 23:30 МСК (20:30 UTC) — ещё 4-е, 00:30 МСК
(21:30 UTC того же дня) — уже 5-е, и 4-е становится прошлым. README: даты
перевода/вывода — по Москве.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:29:00 +04:00
Andrey Epifancev
ef7458eddb feat: аудит входа LOGIN / LOGIN_FAILED (OQ-64, #9 п.5)
Раздел SESSION, object_type app_user, IP и user-agent запроса
(`services/session_audit.py`):
- LOGIN — выдача dev-токена (в транзакции запроса) и первый успешный
  `current_user` пользователя после старта процесса (`SessionAudit.logged_in`,
  в памяти процесса; параллельные первые запросы — одно событие);
- LOGIN_FAILED — 401 (токен не принят: user_id пуст, причина без токена) и 403
  аутентификации (не зарегистрирован / отключён / неоднозначен: email в
  description, user_id — если найден). `AuthenticationFailedError` (подкласс
  HTTPException) обрабатывается после отката транзакции запроса, запись — в
  отдельной транзакции `session_transaction`, поэтому остаётся в журнале.
Без заголовка Authorization и 403 «недостаточно прав» — не события входа;
ошибка записи аудита входа ответ не меняет.

Тесты: `test_login_audit.py` (в т.ч. фиксация при откате запроса на
`committed_app`); счётчики следов импорта не учитывают события SESSION.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:27:38 +04:00
Andrey Epifancev
2017d11379 chore: .dockerignore исключает api/.env.* (OQ-82, #9 п.4)
`api/.env.test`, `api/.env.example` и прочие env-файлы больше не попадают в
образ (как у tabular-parser и scan-gateway): настройки контейнера задаются
переменными окружения.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:27:38 +04:00
Andrey Epifancev
4b873468fb fix: GET / без редиректа на //, 405 на неверный метод под /api/v1 (OQ-61, OQ-62, #9 п.3)
Причина редиректа: `FastAPI(root_path="/")` — путь маршрута для `GET /` после
вычитания root_path пуст, монтирование SPA его не находит, и роутер Starlette
отвечал 307 на `//`. Теперь `root_path` нормализуется (`/` → "", хвостовой `/`
снимается) — `_normalize_root_path` существовал, но не применялся.

`SPAStaticFiles` для путей `api/...` сверяется с маршрутами приложения: путь
совпал, метод нет (`Match.PARTIAL`) — 405 с заголовком `Allow` и русским
`detail`; иначе — прежний 404 (dev-маршруты при выключенных флагах — 404).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:27:38 +04:00
Andrey Epifancev
0eedb822e2 fix: /docs и OpenAPI только при DEBUG, CORS из CORS_ORIGINS в проде (OQ-65, OQ-66, #9 п.2)
Приложение собирает `create_app(settings, web_dir=...)`: при DEBUG=false нет
`/docs`, `/redoc`, `/openapi.json` (SPA-заглушка на эти пути отвечает 404, а не
index.html); CORS — `cors_options`: DEBUG — `*` с credentials, иначе
`CORS_ORIGINS` (пусто — middleware не подключается, credentials только для
списка без `*`). Настройка `CORS_ORIGINS` читает JSON-список или строку через
запятую. `.env.example` и README «Конфигурация» дополнены.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:27:38 +04:00
Andrey Epifancev
5e11f0d0bf fix: seed --dev только при DEBUG=true (OQ-60, #9 п.1)
CLI `python -m scripts.seed --dev` сверяется с `DEBUG` (`check_dev_allowed`) до
чтения seed и подключения к БД; при `DEBUG=false` — код возврата 2 и сообщение
«--dev доступен только при DEBUG=true (dev-пользователи не для прода)».
`run_seed(session, dev=True)` защиты не имеет (её вызывают тесты).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 13:13:30 +04:00