Технический долг Среза 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
Owner

Closes #9. Bounded-задача параллельно Срезу 2a (без OpenSpec change; спеки openspec/specs/ синхронизированы с кодом в этой же ветке). 13 коммитов: 7 по пунктам дизайна + 6 по ревью.

Что сделано

  1. seed --dev только при DEBUG=true (иначе код 2 до чтения seed и подключения к БД) — OQ-60.
  2. /docs, /redoc, /openapi.json только при DEBUG; CORS: в dev * без credentials, в проде из CORS_ORIGINS (пусто — кросс-доменные запрещены) — OQ-66, часть OQ-65.
  3. GET / отдаёт SPA без редиректа на // (причина — root_path="/", теперь нормализуется); неверный метод на существующий маршрут /api/v1/* — 405 с Allow, несуществующий путь — 404 — OQ-61, OQ-62 (редирект).
  4. .dockerignore исключает api/.env.* — OQ-82.
  5. Аудит входа: LOGIN (выдача dev-токена; первый успешный запрос пользователя после старта процесса), LOGIN_FAILED (401 «токен не принят», 403 аутентификации) — раздел SESSION, отдельная транзакция (транзакция запроса при ошибке откатывается), токен в журнал не пишется, user_agent/description обрезаются — OQ-64.
  6. X-Forwarded-For только от доверенных прокси: настройка FORWARDED_ALLOW_IPS (по умолчанию 127.0.0.1) — вторая часть OQ-65; остаток к платформе: перечень прокси прода.
  7. «Сегодня» для effective_date — дата Europe/Moscow — follow-up #8.
  8. Seed матрицы role_permission: в пустую таблицу — вся матрица; в заполненную — только пары новых в этом прогоне ролей/прав; снятые администратором связи не возвращаются — follow-up #8.

Проверки

make check-full EXIT 0: ruff/format/mypy чисто (87 файлов), unit 825 passed, dbml-check пусто, integration 290 passed, docker-build ok, web-check (biome 24 файла, tsc, rsbuild) чисто. Ревью (opus): approve with fixes → все 4 Important исправлены (R-1 длина полей, R-2 доверенные прокси, R-3 новые права в заполненную матрицу, R-4 спеки), плюс R-7, R-9, R-11.

Rulings I made (без обсуждения)

  • Проверка DEBUG для --dev — только в CLI; run_seed(dev=True) для тестов без защиты.
  • Пустой CORS_ORIGINS вне DEBUG — middleware CORS не подключается; при DEBUG * без allow_credentials (токен в заголовке, cookie не используются).
  • SPA-заглушка отвечает 404 на пути документации при DEBUG=false; текст 404 под /api/v1 остался «Not Found» (на него опирается тест Среза 0), 405 — по-русски.
  • Запрос без Authorization или со схемой не Bearer — не попытка входа (боты не засоряют журнал); «аутентификация не настроена» при предъявленном токене — LOGIN_FAILED; отказ dev-токена (404/409) не аудируется.
  • LOGIN первого запроса — в отд��льной транзакции (иначе терялся бы при ошибке запроса после аутентификации); учёт вошедших — множество в памяти процесса (один процесс на инстанс).
  • Ошибка записи события входа логируется и не меняет ответ.
  • Без tzdata — фиксированный UTC+3 (в python:3.11-slim Europe/Moscow есть).
  • Пограничный случай: администратор снял все связи матрицы → seed зальёт её заново (документировано).

Deferred (в follow-up issue при закрытии)

  • R-1 остаток: троттлинг/дедуп LOGIN_FAILED по (ip, причина) или решение «режет шлюз».
  • R-5: LOGIN первого запроса берёт второе соединение из пула; при всплеске после рестарта возможна потеря события (логируется).
  • R-6: LOGIN пишется при каждой выдаче dev-токена (dev-вход = два события).
  • R-8: 404 статики//api/v1 и 405 от StaticFiles на POST / — английские тексты.
  • R-10: create_app(settings) — lifespan/S3/NATS читают модульный settings.
  • R-12: CORS_ORIGINS без нормализации хвостового /.
  • R-13: тесты — LOGIN_FAILED для неоднозначной учётной записи и «аутентификация не настроена»; create_app ставит SessionAudit; HEAD/OPTIONS под /api/v1.
  • OQ-62 остаток: фронт не собирается в образ arm-core.
  • OQ-65 остаток: перечень доверенных прокси прода — к платформе.
  • CLAUDE.md, пункт «Аудит»: «в текущей транзакции» — уточнить исключением для SESSION.

🤖 Generated with Claude Code

https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2

Closes #9. Bounded-задача параллельно Срезу 2a (без OpenSpec change; спеки `openspec/specs/` синхронизированы с кодом в этой же ветке). 13 коммитов: 7 по пунктам дизайна + 6 по ревью. ## Что сделано 1. `seed --dev` только при `DEBUG=true` (иначе код 2 до чтения seed и подключения к БД) — OQ-60. 2. `/docs`, `/redoc`, `/openapi.json` только при `DEBUG`; CORS: в dev `*` без credentials, в проде из `CORS_ORIGINS` (пусто — кросс-доменные запрещены) — OQ-66, часть OQ-65. 3. `GET /` отдаёт SPA без редиректа на `//` (причина — `root_path="/"`, теперь нормализуется); неверный метод на существующий маршрут `/api/v1/*` — 405 с `Allow`, несуществующий путь — 404 — OQ-61, OQ-62 (редирект). 4. `.dockerignore` исключает `api/.env.*` — OQ-82. 5. Аудит входа: `LOGIN` (выдача dev-токена; первый успешный запрос пользователя после старта процесса), `LOGIN_FAILED` (401 «токен не принят», 403 аутентификации) — раздел `SESSION`, отдельная транзакция (транзакция запроса при ошибке откатывается), токен в журнал не пишется, `user_agent`/`description` обрезаются — OQ-64. 6. `X-Forwarded-For` только от доверенных прокси: настройка `FORWARDED_ALLOW_IPS` (по умолчанию `127.0.0.1`) — вторая часть OQ-65; остаток к платформе: перечень прокси прода. 7. «Сегодня» для `effective_date` — дата `Europe/Moscow` — follow-up #8. 8. Seed матрицы `role_permission`: в пустую таблицу — вся матрица; в заполненную — только пары новых в этом прогоне ролей/прав; снятые администратором связи не возвращаются — follow-up #8. ## Проверки `make check-full` EXIT 0: ruff/format/mypy чисто (87 файлов), unit 825 passed, dbml-check пусто, integration 290 passed, docker-build ok, web-check (biome 24 файла, tsc, rsbuild) чисто. Ревью (opus): approve with fixes → все 4 Important исправлены (R-1 длина полей, R-2 доверенные прокси, R-3 новые права в заполненную матрицу, R-4 спеки), плюс R-7, R-9, R-11. ## Rulings I made (без обсуждения) - Проверка `DEBUG` для `--dev` — только в CLI; `run_seed(dev=True)` для тестов без защиты. - Пустой `CORS_ORIGINS` вне DEBUG — middleware CORS не подключается; при DEBUG `*` без `allow_credentials` (токен в заголовке, cookie не используются). - SPA-заглушка отвечает 404 на пути документации при `DEBUG=false`; текст 404 под `/api/v1` остался «Not Found» (на него опирается тест Среза 0), 405 — по-русски. - Запрос без `Authorization` или со схемой не Bearer — не попытка входа (боты не засоряют журнал); «аутентификация не настроена» при предъявленном токене — `LOGIN_FAILED`; отказ dev-токена (404/409) не аудируется. - `LOGIN` первого запроса — в отд��льной транзакции (иначе терялся бы при ошибке запроса после аутентификации); учёт вошедших — множество в памяти процесса (один процесс на инстанс). - Ошибка записи события входа логируется и не меняет ответ. - Без tzdata — фиксированный UTC+3 (в `python:3.11-slim` `Europe/Moscow` есть). - Пограничный случай: администратор снял **все** связи матрицы → seed зальёт её заново (документировано). ## Deferred (в follow-up issue при закрытии) - R-1 остаток: троттлинг/дедуп `LOGIN_FAILED` по (ip, причина) или решение «режет шлюз». - R-5: `LOGIN` первого запроса берёт второе соединение из пула; при всплеске после рестарта возможна потеря события (логируется). - R-6: `LOGIN` пишется при каждой выдаче dev-токена (dev-вход = два события). - R-8: 404 статики/`/api/v1` и 405 от StaticFiles на `POST /` — английские тексты. - R-10: `create_app(settings)` — lifespan/S3/NATS читают модульный `settings`. - R-12: `CORS_ORIGINS` без нормализации хвостового `/`. - R-13: тесты — `LOGIN_FAILED` для неоднозначной учётной записи и «аутентификация не настроена»; `create_app` ставит `SessionAudit`; HEAD/OPTIONS под `/api/v1`. - OQ-62 остаток: фронт не собирается в образ `arm-core`. - OQ-65 остаток: перечень доверенных прокси прода — к платформе. - CLAUDE.md, пункт «Аудит»: «в текущей транзакции» — уточнить исключением для SESSION. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
aep added 13 commits 2026-10-05 12:53:30 +03:00
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
Приложение собирает `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
Причина редиректа: `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
`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
Раздел 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
`_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
`_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
`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
`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
При непустой `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
`*` вместе с 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
Формулировка уточнена в докстрингах `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
- 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
aep merged commit ee40eb184e into main 2026-10-05 19:18:44 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/arm-core#10
No description provided.