diff --git a/context/03-architecture.md b/context/03-architecture.md index 456459a..0c1d0bf 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -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, слои, ошибки, логи diff --git a/context/07-open-questions.md b/context/07-open-questions.md index da8b9b9..dc24e3b 100644 --- a/context/07-open-questions.md +++ b/context/07-open-questions.md @@ -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` надо сбрасывать | diff --git a/dev/contour-transfer-guide.md b/dev/contour-transfer-guide.md index 7d6940c..1a7d81b 100644 --- a/dev/contour-transfer-guide.md +++ b/dev/contour-transfer-guide.md @@ -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. Чек-лист одной синхронизации