Гайд переноса §6: настройки контура через OPENBAO__APP__*, dev-заглушка аутентификации (#52)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
This commit is contained in:
Andrey Epifancev 2026-10-07 18:41:48 +04:00
parent a33c5b563e
commit 99cbcffea5
3 changed files with 65 additions and 22 deletions

View File

@ -215,6 +215,9 @@ lifecycle-правил платформы); объект, записанный
по ЗО (`business_segment`), допустимость действия по статусу версии (`consolidation_status`) и по активной
блокировке (`consolidation_lock`). Право говорит «может», policy — «эту ли и сейчас ли».
5. Локальная разработка: заглушка-эмитент JWT с теми же claims (feature-flag `AUTH_DEV_ISSUER`).
6. **Контур банка — временно тоже на заглушке** (`DEBUG=true` + `AUTH_DEV_ISSUER=true`, вход по email через
`POST /api/v1/dev/token`): параметры JWT РАИСА неизвестны (OQ-54), платформенной аутентификации в контуре пока нет.
Подробности и последствия — `dev/contour-transfer-guide.md`, раздел 6.3.
## Слои arm-core (по раскладке шаблона команды)
@ -303,6 +306,9 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро
`OPENBAO__MINIO__{MINIO_URL, MINIO_ADMIN_LOGIN, MINIO_ADMIN_PASSWORD, MINIO_BUCKET}`,
`OPENBAO__NATS__{NATS_URL, NATS_SEED, NATS_JWT}`, `OPENBAO__SMB__{SMB_HOST, SMB_PATH, SMB_LOGIN, SMB_PASSWORD}`.
Прочее: `DEBUG`, `ROOT_PATH`, `VERSION`, `SERVER__HOST`, `SERVER__PORT`.
- Платформа РАИСА передаёт в под только секреты OpenBao, поэтому любая настройка верхнего уровня `Settings` в контуре
задаётся как `OPENBAO__APP__<ИМЯ>` (приоритет: обычный env → `OPENBAO__APP__*` → `.env` → умолчание; неизвестное
имя — ошибка старта; `OPENBAO__APP__OPENBAO__…` запрещено). Контурные значения — `dev/contour-transfer-guide.md`, раздел 6.
- `settings` создаётся при импорте (`core/config.py`, `db/db.py`, `app.py`) — импорт почти любого модуля требует
полного окружения; тесты получают его из `.env.test`.
- `DEBUG=true` — режим разработки шаблона: NATS не подключается, middleware РАИСА не импортируется. Наши флаги
@ -329,7 +335,7 @@ FastAPI» платформы РАИСА/aurora): дерево импортиро
работают на семействе `files.*` (`files.tabular_uploaded`, `files.tabular_parsed`, `files.img_parsed`).
- **SMB** (горячие папки ИС Сканирование) — `smbprotocol`/`smbclient`, сессия регистрируется в lifespan; нужен только `scan-gateway`.
- **Внешняя аутентификация** — пакет `raisa_fastapi_protected_api` из внутреннего Nexus (`AuthorizationMiddleware`,
`ProtectedSettings`, …), импортируется только при `DEBUG=false`; фронтовая пара — `raisa-fastapi-protected-web`.
`ProtectedSettings`, …), импортируется только при `DEBUG=false` (в контуре временно `DEBUG=true`, см. «Аутентификация и авторизация», п. 6); фронтовая пара — `raisa-fastapi-protected-web`.
Вне сети банка оба пакета недоступны (см. вопросы Среза 0).
### DI, слои, ошибки, логи

View File

@ -59,7 +59,7 @@
| OQ-51 | `required_document_rule`: уникальный индекс `(approach, document_type, account_id)` при nullable `account_id` допускает дубли строк для не-ОСВ документов (NULL в PostgreSQL различны). Добавить `NULLS NOT DISTINCT`? | DBML v5.13 (`required_document_rule`, Indexes); arm-core: предл. 4 (T8.3) | срез 3 (чек-лист); кандидат DBML v5.14 | допущение принято: ORM и миграция 1:1 с DBML v5.13 (без `NULLS NOT DISTINCT`); предложение v5.14: `[unique, note: 'NULLS NOT DISTINCT']`, как у `document` |
| OQ-52 | Контракт v1.0, `recognition-result-message`: `schema_version` принимает только `1.x` (`^1\.\d+$`), а `result_schema_version` — любой MAJOR (`^\d+\.\d+$`). Намеренно (файл результата версионируется отдельно) или небрежность? | `contracts/recognition-result-message.schema.json`; arm-core: предл. 5 (T20.7) | контракт v1.x; срез 2 (inbox, маппинг) | допущение принято: ядро повторяет схему буквально (любой MAJOR проходит валидацию); совместимость MAJOR результата проверяет маппинг Среза 2; уточнить у владельца контракта |
| OQ-53 | Контракт v1.0: схемы нигде не задают `additionalProperties: false` (кроме `columns` ОСВ). Считать ли неизвестные поля допустимыми расширениями? | `contracts/*.schema.json`; arm-core: предл. 6 (T20) | контракт v1.x; все три сервиса | допущение принято: модели ядра `extra = "ignore"` (строже схемы быть нельзя — ядро отвергнет совместимого пира); изменение — через MINOR контракта |
| OQ-54 | Параметры JWT РАИСА неизвестны: JWKS URL / публичный ключ, `iss`, `aud`, имя claim с email. Что в продуктивном контуре? Делать ли проверку `aud`/`iss` обязательной в проде? | ТЗ 8.1, `03-architecture.md` (SSO РАИСА); `core/auth.py`; arm-core: предл. 7 (T16.2) | срез 1+ (прод-конфигурация) | допущение принято: настраиваются `AUTH_JWKS_URL`/`AUTH_PUBLIC_KEY` (взаимоисключающие), `AUTH_ISSUER`, `AUTH_AUDIENCE`, `AUTH_EMAIL_CLAIM` (по умолчанию `email`); при пустых `AUTH_AUDIENCE`/`AUTH_ISSUER` `aud`/`iss` не проверяются; предложение: в проде сделать `aud` обязательным и предупреждать при старте, если пусто |
| OQ-54 | Параметры JWT РАИСА неизвестны: JWKS URL / публичный ключ, `iss`, `aud`, имя claim с email. Что в продуктивном контуре? Делать ли проверку `aud`/`iss` обязательной в проде? | ТЗ 8.1, `03-architecture.md` (SSO РАИСА); `core/auth.py`; arm-core: предл. 7 (T16.2) | срез 1+ (прод-конфигурация) | допущение принято: настраиваются `AUTH_JWKS_URL`/`AUTH_PUBLIC_KEY` (взаимоисключающие), `AUTH_ISSUER`, `AUTH_AUDIENCE`, `AUTH_EMAIL_CLAIM` (по умолчанию `email`); при пустых `AUTH_AUDIENCE`/`AUTH_ISSUER` `aud`/`iss` не проверяются; предложение: в проде сделать `aud` обязательным и предупреждать при старте, если пусто; **контекст 07.10.2026:** контур временно работает на dev-заглушке аутентификации (`DEBUG=true`, `AUTH_DEV_ISSUER=true`, вход по email; `dev/contour-transfer-guide.md` §6.3); не выяснено, используется ли в контуре `raisa_fastapi_protected_api`; вероятно, Keycloak (у старого фронта `REACT_APP_KEYCLOAK_URL`); переход на SSO — после ответа и arm-core#43 |
| OQ-55 | В enum `audit_action` нет действия для смены статуса задания распознавания (`recognition_job`). Добавлять ли (DBML v5.14) и нужен ли аудит обмена вообще? | DBML v5.13 `audit_action`; arm-core: предл. 8 (R-7, design #20 arm-core) | срез 2 (обмен); кандидат DBML v5.14 | допущение принято: Срез 0 не пишет `audit_event` из обмена `recognition_job`; сервис аудита (`services/audit.py`) готов и покрыт тестами; если нужно — действие в v5.14 и вызовы в `recognition_exchange.py` |
| OQ-56 | Append-only аудит обеспечен только на уровне кода (единственная точка записи, статический тест на UPDATE/DELETE по `AuditEvent`). Нужна ли защита на уровне БД (триггер или `REVOKE UPDATE, DELETE` для роли приложения)? Вопрос поднят дважды (в том числе финальным ревью ветки). | инвариант «Append-only аудит» (`../CLAUDE.md`); `services/audit.py`; arm-core: предл. 9 и дополнение к нему (задача 21) | миграции, права БД; платформа | **решено человеком 04.10.2026: осознанный техдолг.** Append-only на уровне кода и тестов; защита на уровне БД (триггер или `REVOKE UPDATE, DELETE`) — позже, вместе с платформой, до прода |
| OQ-57 | Счётчик повторных публикаций запроса (`ACK_TIMEOUT_SECONDS` × `OUTBOX_MAX_REPUBLISH`) хранится в `recognition_job.metrics.republish_count` (jsonb), колонки в DBML нет; при каждой повторной публикации `published_at` перезаписывается временем последней, время первой не сохраняется. Нужны ли колонка счётчика и время первой публикации (v5.14)? | `services/recognition_exchange.py` (design #6 arm-core); arm-core: предл. 10 (T23, T23.1); docs: O-19 (задача 4 фазы 2) | срез replay; кандидат DBML v5.14 | допущение принято: счётчик в `metrics.republish_count`, `attempt` при повторе не растёт; `published_at` — время последней публикации (`dlq` через (`OUTBOX_MAX_REPUBLISH`+1)×`ACK_TIMEOUT_SECONDS` ≈ 120 с, описано в `dev/README.md`); при будущем replay счётчик и `reply_deadline` надо сбрасывать |

View File

@ -45,10 +45,10 @@ Gitea в контуре нет)
- Nexus `nexus.dev.raisa.go.rshbank.ru` (`setup/pip.conf`, `setup/.npmrc`) и пакеты `raisa_fastapi_protected_api`
(`api/requirements.txt` arm-core), `raisa-fastapi-protected-web`, `raisa-ui` (`web/package.json`, `optionalDependencies`).
Снаружи `make venv` их пропускает (`grep -v '^raisa_'`), импорт `raisa_fastapi_protected_api` условный — только при
`DEBUG=false`. В контуре всё ставится штатно.
- Запуск платформой: `api/run_server.py`, `SERVER__HOST`/`SERVER__PORT`, ресурсы `raisa_config.yml`, секреты `OPENBAO__*`
из OpenBao, Python 3.11 в базовом образе.
- PostgreSQL 16 (схема `dkb`), NATS без JetStream (nkeys/JWT), S3-совместимое хранилище, JWT РАИСА для входа.
`DEBUG=false` (в контуре временно `DEBUG=true`, см. 6.3). В контуре всё ставится штатно.
- Запуск платформой: `api/run_server.py`, `SERVER__HOST`/`SERVER__PORT`, ресурсы `raisa_config.yml`, секреты и настройки `OPENBAO__*`
из OpenBao (раздел 6), Python 3.11 в базовом образе.
- PostgreSQL 16 (схема `dkb`), NATS без JetStream (nkeys/JWT), S3-совместимое хранилище, вход временно через dev-заглушку, позже JWT РАИСА (раздел 6.3).
## 3. Подготовка снимка: `scripts/prepare-contour-snapshot.sh`
@ -126,23 +126,60 @@ compose из `docs` либо Docker. Интеграционные тесты п
## 6. Что нужно от платформы для запуска
Переменные окружения — `api/.env.example` каждого сервиса (имена и смысл), секреты под префиксом `OPENBAO__*`.
Минимум для arm-core:
### 6.1. Как задаются настройки: только через OpenBao
- `OPENBAO__DATABASE__DATABASE_URL` (asyncpg, схема `dkb`), `OPENBAO__MINIO__{MINIO_URL, MINIO_ADMIN_LOGIN,
MINIO_ADMIN_PASSWORD, MINIO_BUCKET}` (`MINIO_SECURE=true` по умолчанию), `OPENBAO__NATS__{NATS_URL, NATS_SEED, NATS_JWT}`.
- `DEBUG=false` (включает middleware РАИСА, выключает `/docs` и dev-токены), `CORS_ORIGINS` (явный список; пусто —
кросс-доменные запросы запрещены), `FORWARDED_ALLOW_IPS` (адреса прокси платформы, иначе IP в аудите — адрес прокси),
`AUTH_JWKS_URL` или `AUTH_PUBLIC_KEY` + `AUTH_ISSUER`, `AUTH_AUDIENCE`, `AUTH_EMAIL_CLAIM` — параметры JWT РАИСА (открытый
вопрос OQ в `context/07-open-questions.md`). `AUTH_DEV_ISSUER`/`AUTH_DEV_SECRET` в проде не задавать.
- Миграции и seed при деплое: из `api/` — `alembic upgrade head`, затем `python -m scripts.seed` (справочники: план счетов,
правила элиминации, роли и права; `--dev` только для стенда разработки). Те же команды — цели `make migrate` и `make seed` (`README.md` arm-core,
«Быстрый старт», шаг 4 «Миграции и справочники»).
Платформа РАИСА передаёт в под **только секреты OpenBao**; обычные переменные окружения (`DEBUG`, `NATS_ENABLED`, ...)
задать нельзя (ответ разработчика переноса, 07.10.2026). Поэтому любая настройка верхнего уровня `Settings` задаётся
как **`OPENBAO__APP__<ИМЯ>`** (arm-core PR #49, issue #44; arm-scan-gateway PR #12, issue #11; arm-tabular-parser PR #23,
issue #22; файл `api/src/core/settings_sources.py` одинаков в трёх сервисах). Правила:
`tabular-parser` и `scan-gateway`: только `OPENBAO__MINIO__*`, `OPENBAO__NATS__*`, `DEBUG`, `NATS_ENABLED=true`; у парсера
`PARSE_MAX_BYTES`, `PARSE_TIMEOUT_SECONDS`; у шлюза `SCAN_HOTFOLDER_MODE=smb` и `OPENBAO__SMB__{SMB_HOST, SMB_PATH,
SMB_LOGIN, SMB_PASSWORD}` (режим `local` и эмулятор `make emulator` — только для разработки). Subjects NATS, схема ключей S3
и ресурсы — `context/03-architecture.md`.
- приоритет источников: обычная переменная окружения → `OPENBAO__APP__*` → `api/.env` → значение по умолчанию;
- `OPENBAO__APP__*` читается только из окружения процесса, в `.env` не действует;
- имя должно существовать в `Settings`; неизвестное имя — ошибка при старте (опечатка не остаётся незамеченной);
- вложенные настройки — через `__`: `OPENBAO__APP__SERVER__PORT`;
- `OPENBAO__APP__OPENBAO__...` запрещено: секреты задаются только как `OPENBAO__<ГРУППА>__<ИМЯ>`.
Имена и смысл настроек — `api/.env.example` каждого сервиса. Секреты (общие для всех сервисов: `OPENBAO__MINIO__*`,
`OPENBAO__NATS__*`) задаются как раньше.
### 6.2. Значения контура по сервисам
| Сервис | Секреты `OPENBAO__<ГРУППА>__*` | Настройки `OPENBAO__APP__*` |
|---|---|---|
| arm-core | `OPENBAO__DATABASE__DATABASE_URL` (asyncpg, схема `dkb`), `OPENBAO__MINIO__{MINIO_URL, MINIO_ADMIN_LOGIN, MINIO_ADMIN_PASSWORD, MINIO_BUCKET}` (`MINIO_SECURE=true` по умолчанию), `OPENBAO__NATS__{NATS_URL, NATS_SEED, NATS_JWT}` | `NATS_ENABLED=true` (по умолчанию `false`: без него NATS не подключается и обмен с сервисами распознавания не работает), `DEBUG=true`, `AUTH_DEV_ISSUER=true`, `AUTH_DEV_SECRET=<случайная строка от 32 байт>` (поле верхнего уровня `Settings`, поэтому тоже через `OPENBAO__APP__`), `FORWARDED_ALLOW_IPS=<адреса прокси платформы>` (иначе IP в аудите — адрес прокси); `CORS_ORIGINS` при `DEBUG=true` не нужен. `SERVER__HOST` не задавать (платформа задаёт сама) |
| scan-gateway | `OPENBAO__MINIO__*`, `OPENBAO__NATS__*`, `OPENBAO__SMB__{SMB_HOST, SMB_PATH, SMB_LOGIN, SMB_PASSWORD}` | `SCAN_HOTFOLDER_MODE=smb` (`NATS_ENABLED` по умолчанию `true`, задавать не нужно). `SCAN_HOTFOLDER_PATH` в контуре не нужен (только режим `local`; режим `local` и эмулятор `make emulator` — для разработки) |
| tabular-parser | `OPENBAO__MINIO__*`, `OPENBAO__NATS__*` | сверх секретов ничего (`NATS_ENABLED` по умолчанию `true`); при необходимости `PARSE_TIMEOUT_SECONDS` (и `PARSE_MAX_BYTES`) |
В отличие от arm-core, в `scan-gateway` и `tabular-parser` `NATS_ENABLED=true` по умолчанию, а `DEBUG` влияет только на
уровень логов; платформенной аутентификации у них нет (внутренние сервисы без HTTP-входа для пользователей).
Зависимости под контур: `PyJWT[crypto]==2.8.0` и `cryptography==43.0.3` (arm-core PR #50, issue #43; в том же PR
исправлена проверка JWKS на PyJWT 2.8.0).
Миграции и seed при деплое: из `api/` — `alembic upgrade head`, затем `python -m scripts.seed` (справочники: план счетов,
правила элиминации, роли и права; `--dev` только для стенда разработки). Те же команды — цели `make migrate` и `make seed`
(`README.md` arm-core, «Быстрый старт», шаг 4 «Миграции и справочники»).
Subjects NATS, схема ключей S3 и ресурсы — `context/03-architecture.md`.
### 6.3. Аутентификация в контуре (временно)
Контур временно **без платформенной аутентификации**: приложение доступно любому, кто его открыл. Вход идёт через
dev-заглушку: `POST /api/v1/dev/token {email}` выдаёт JWT, на фронте её использует форма DevLogin. Что для этого включено:
- `OPENBAO__APP__DEBUG=true` дополнительно включает CORS `*` и открытые `/docs`, `/redoc`, `/openapi.json`;
middleware `raisa_fastapi_protected_api` не импортируется (в arm-core он и так не подключён);
- `OPENBAO__APP__AUTH_DEV_ISSUER=true` подключает роутер dev-токенов (нужны оба флага: `DEBUG` и `AUTH_DEV_ISSUER`);
токены подписываются `AUTH_DEV_SECRET`.
Последствия, которые нужно понимать:
- пользователи с ролями должны быть в `app_user` **до входа** (seed); войти может любой, кто знает email пользователя,
то есть защита контура держится на сетевой изоляции, а не на аутентификации;
- `AUTH_DEV_SECRET` — секрет: случайная строка от 32 байт, хранится в OpenBao, в код и в `.env.example` не попадает.
Переход на SSO РАИСА (отключить `DEBUG`/`AUTH_DEV_ISSUER`, задать `AUTH_JWKS_URL` или `AUTH_PUBLIC_KEY` + `AUTH_ISSUER`,
`AUTH_AUDIENCE`, `AUTH_EMAIL_CLAIM`) — после ответа на OQ-54 (`context/07-open-questions.md`) и задачи arm-core#43.
## 7. Правки внутри контура и обратный поток
@ -156,7 +193,7 @@ git diff <коммит sync> HEAD -- . ':!SNAPSHOT.md' > /tmp/contour-arm-core.p
Патч применяется снаружи в ветку и проходит обычный PR; после его мержа следующий снимок уже содержит правку, и
`rsync --delete` ничего не потеряет. Если правка касается только контура (например, секретов платформы), её место — вне
репозитория: переменные окружения OpenBao, а не код.
репозитория: значения в OpenBao (`OPENBAO__*`), а не код.
## 8. Чек-лист одной синхронизации