Реализованы сервисы ВМ2 - проверка сообщений и синхронизация с Б24 (деплой еще без перевода в боевой режим)

This commit is contained in:
mi
2026-08-13 18:52:42 +03:00
parent 5100ba9fc3
commit 99605b1c77
144 changed files with 15295 additions and 1120 deletions
+77 -47
View File
@@ -2,6 +2,7 @@
> Термины — в [`arch-00-glossary.md`](arch-00-glossary.md).
> Приоритет документов — в [`README.md`](README.md).
> Безопасность размещения на VM, OS-роли, SSH, секреты и production-деплой — в [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md).
## Назначение
@@ -47,8 +48,8 @@ HAN Chat - приложение для мигрантов, где стартов
- SMS Service: internal durable order API, шаблоны и бессрочный журнал SMS; отдельный worker вызывает i-Digital Direct, callback обновляет только журнал.
- api-backend: Python-приложение с REST API, realtime-доставкой сообщений и бизнес-логикой.
- Notification producers: сервисы приватной сети, создающие/отменяющие персональные уведомления через Internal API с отдельным Bearer token на `source`; `producer_test` используется только для smoke API.
- Nginx Reverse Proxy: единая публичная точка входа, HTTPS termination и маршрутизация на Keycloak/API/frontend web/Bitrix24.
- Message Safety Service: отдельный сервис проверки входящих сообщений; вызов из API`200 allow` | `403 deny` | `203 pending` + `task_id` (при `203` api-backend синхронно поллит task до финального вердикта, без очереди анализа на api-backend).
- Nginx Reverse Proxy: независимые точки входа ВМ1 и ВМ2; ВМ1 обслуживает приложение/Open Lines, ВМ2 — CRM webhook `bitrix-sync` и private Message Safety API.
- Message Safety Service: отдельный сервис ВМ2 проверки исходящих сообщений; target v2`200 allow` | `403 deny` | `202 pending` + `Location`.
- Bitrix24 Local App: локальное приложение, custom connector `han_mobile_app` для Bitrix24 Open Lines: чат, OAuth, webhook-события, маппинг `dialog_id``bitrix_chat_id`.
- Bitrix24 sync service: двусторонняя синхронизация App DB ↔ Битрикс24 CRM (Contact на MVP; маппинг ID, очередь через триггеры, webhook от роботов Bitrix24).
- Managed PostgreSQL (приватная сеть, одна база): схемы `han_app`, `bitrix_sync`, `bitrix_local`, `keycloak`, `message_safety`, `sms` — отдельный DB-user на схему.
@@ -59,26 +60,50 @@ HAN Chat - приложение для мигрантов, где стартов
## Инфраструктура развёртывания (зафиксировано)
На первом этапе весь backend-контур работает на **одной VM** в облаке провайдера:
Production-like backend разделён на два контура в одной private network/VPC:
- `nginx`, `api-backend`, `message-safety`, `keycloak`, `sms-service`/worker, `bitrix-sync`, `bitrix-local-app`, `redis`, `otel-collector` — в Docker Compose на VM;
- публичный доступ из интернета только через `nginx` (порты 80/443);
- внутренние сервисы общаются по Docker-сети на localhost VM.
- **ВМ1 HAN Chat**: edge `nginx`, `api-backend`, `keycloak`, `sms-service`/worker, `bitrix-local-app`, Redis DB0/DB1 и локальный `otel-collector`;
- **ВМ2 Processing**: собственный public/private `nginx`, `message-safety` API/worker, `clamd`/`freshclam`, `bitrix-sync`, отдельный Redis Safety и локальный `otel-collector`;
- каждая VM имеет один root Compose project и отдельный root-owned systemd deployment unit;
- ВМ1 и ВМ2 имеют независимые public DNS/TLS ingress на своих nginx; ВМ2 публикует только exact CRM webhook;
- ВМ1 вызывает ВМ2 по private HTTPS с проверкой internal CA, service token, cloud SG и host firewall;
- Битрикс24 вызывает public nginx ВМ2 напрямую; CRM webhook не проходит через ВМ1 и не создаёт на ней трафик/зависимость.
ВМ2 является независимым контуром вспомогательных сервисов. При её недоступности отправка пользовательских сообщений и CRM sync приостанавливаются, но чтение истории, auth, realtime и приём сообщений оператора на ВМ1 продолжаются. Недоступность ВМ1 не мешает ВМ2 принимать CRM webhook и выполнять накопленные workflows. Fail-open для Message Safety запрещён.
Базы данных — **managed PostgreSQL** того же провайдера в **том же облачном кластере/VPC**, **без публичного доступа** из интернета. VM подключается к БД только по приватной сети.
Размещение нескольких сервисов на одной VM не делает их одним доверенным контуром. Для каждого контейнера сохраняются least privilege, отдельные секреты, минимальные Docker networks и запрет доступа к Docker socket/host root. Обязательный baseline VM и контейнеров — [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md).
Схема данных в managed PostgreSQL (перечень таблиц внутри схем — в модульных спецификациях, не в arch-*):
| База / схема | Сервисы | Назначение схемы |
|---|---|---|
| одна база / `han_app` | `api-backend`, `bitrix-sync` (ограниченный GRANT) | прикладные данные приложения, очередь sync, audit |
| одна база / `bitrix_sync` | `bitrix-sync` | worker state, retry/dead letter, sync audit |
| одна база / `message_safety` | `message-safety` | verdict cache, safety_task, rule config |
| одна база / `message_safety` | `message-safety` | verdict caches, safety tasks/audit, immutable versioned runtime config |
| одна база / `bitrix_local` | `bitrix-local-app` | OAuth, inbox, `dialog_sessions` |
| одна база / `keycloak` | Keycloak | учётные записи, realm, сессии IdP |
| одна база / `sms` | `sms-service`, `sms-worker` | шаблоны, runtime settings, бессрочный журнал отправки/доставки SMS |
Redis на первом этапе остаётся на VM в Docker (ephemeral/coordination). Selectel S3 — внешнее object storage: три бакета (`han-chat-quarantine`, `han-chat-attachments`, `han-chat-documents`); см. [`arch-00-glossary.md`](arch-00-glossary.md).
Redis разделён по deployment boundary: DB0/DB1 остаются на ВМ1, отдельный Redis Safety находится на ВМ2. Оба являются ephemeral/coordination слоями; PostgreSQL остаётся durable source of truth. Selectel S3 — внешнее object storage: три бакета (`han-chat-quarantine`, `han-chat-attachments`, `han-chat-documents`); см. [`arch-00-glossary.md`](arch-00-glossary.md).
```mermaid
flowchart LR
client[Client] --> edge[VM1_EdgeNginx]
edge --> api[VM1_ApiBackend]
bitrix[Bitrix24] -->|"CRM webhook HTTPS"| publicGateway[VM2_PublicNginx]
publicGateway --> sync[BitrixSync]
api -->|"HTTPS 8443 + service token"| privateGateway[VM2_PrivateListener]
privateGateway --> safety[MessageSafetyApi]
safety --> worker[SafetyWorker]
worker --> clamd[Clamd]
worker --> s3q[S3Quarantine]
worker --> pg[ManagedPostgreSQL]
sync --> pg
sync --> bitrix
collector[VM2_OtelCollector] --> signoz[PrivateSigNoz]
```
## Контекстная схема
@@ -179,10 +204,10 @@ Frontend не должен:
- хранение истории диалогов;
- запись данных профиля в App DB (синхронизация с Bitrix24 — триггеры → `sync_queue``bitrix-sync`, без участия api-backend);
- выдачу **presigned URL** на загрузку в S3-quarantine, проверку объекта при `complete`, promote/delete после вердикта;
- синхронный вызов Message Safety Service (`POST /internal/safety/v1/messages/check`) и интерпретацию ответа: `200 allow`, `403 deny`, `203 pending` + `task_id`;
- вызов Message Safety v2 (`POST /internal/safety/v2/messages/check`) и интерпретацию `200 allow`, `403 deny`, `202 pending`;
- при `200`: перенос файлов quarantine → S3-data, сохранение сообщения, отправка в Bitrix24;
- при `403`: удаление файлов из quarantine, безопасный ответ клиенту;
- при `203`: api-backend **синхронно поллит** `GET /internal/safety/v1/messages/tasks/{task_id}` до финального `200`/`403` (timeout budget — arch-04), затем promote/Bitrix или cleanup, и только после этого отвечает клиенту финальным результатом;
- при `202`: api-backend **синхронно поллит** `Location` до финального `200`/`403`, terminal failed или timeout, затем promote/Bitrix или cleanup;
- это **ожидание в рамках одного клиентского HTTP-соединения**, а не общая очередь: другие запросы обрабатываются параллельно (workers/async);
- решение «быстрая проверка / долгая» принимает только `message-safety`; на api-backend **нет** очереди анализа сообщений;
- запись checkpoint в `safety_tasks` (App DB) на время poll — для recovery при timeout/crash (I1);
@@ -218,13 +243,15 @@ Frontend не должен:
### Bitrix24 sync service
Отвечает за **двустороннюю** синхронизацию данных между App DB и Битрикс24 CRM:
Отвечает за асинхронную двустороннюю синхронизацию данных между App DB и Битрикс24 CRM по контракту [`../modules/module-07-bitrix-sync.md`](../modules/module-07-bitrix-sync.md):
- **маппинг ID** сущностей приложения ↔ Bitrix24 (`bitrix_contact_id`, `entity_external_mapping`);
- **App DB → Bitrix24:** обработка очереди `sync_queue` (триггеры App DB) — map/create Contact по телефону, push обновлений полей;
- **Bitrix24 → App DB:** приём webhook от роботов Bitrix24, обновление профиля с GUC `han.sync_suppress`;
- реестр синхронизируемых сущностей (MVP: Contact; post-MVP: Lead, Deal, Document);
- повторные попытки, rate limiting Bitrix REST, dead letter;
- **канонический mapping** и его историю в `bitrix_sync.entity_external_mapping`; App DB не хранит CRM Contact ID;
- **App DB → Bitrix24:** durable workflow для `contact.map_or_create`, `contact.update`, `contact.deactivate`;
- **исправление связи:** audited административный запрос запускает `contact.rebind`; прямой `UPDATE` mapping запрещён;
- **Bitrix24 → App DB:** durable webhook inbox, coalescing и reconciliation; запись профиля с transaction-local GUC `han.sync_suppress`;
- mastership по полям: телефон — App/Keycloak, `NAME`/citizenship/email — Битрикс24;
- batch, общий portal rate limiter, leases/fencing, retry до 24 часов и technical DLQ;
- business conflicts через смарт-процесс Битрикс24, technical failures через SigNoz;
- прямой доступ к схеме `han_app` и собственной `bitrix_sync`.
Не отвечает за:
@@ -290,12 +317,12 @@ Confidential **backend client** Keycloak (client credentials) в MVP **не об
- HTTP-контракт для api-backend:
- `200` — синхронная проверка завершена, **allow**;
- `403` — синхронная проверка завершена, **deny**;
- `203` + `task_id` — нужна async-проверка (обычно файлы), сообщение в обработке;
- финальный вердикт async-задачи по `GET /internal/safety/v1/messages/tasks/{task_id}`: `200 allow` | `403 deny` | `203 pending`;
- `202` + `task_id`/`Location` — нужна async-проверка;
- task GET: `200 allow` | `403 deny` | `202 pending` | terminal failed `503`;
- SHA-256 хеширование и lookup кэша вердиктов;
- отдельный pipeline проверки ссылок;
- запись verdict cache, `safety_task` и audit в схеме `message_safety`;
- internal API: `POST /internal/safety/v1/messages/check`, `GET /internal/safety/v1/messages/tasks/{task_id}`.
- запись verdict caches, `safety_task`, audit и immutable `config_versions` в схеме `message_safety`; runtime role не активирует config;
- target internal API: `POST /internal/safety/v2/messages/check`, `GET /internal/safety/v2/messages/tasks/{task_id}`.
Не отвечает за:
@@ -423,7 +450,7 @@ api-backend не решает, sync или async нужна проверка в
1. Frontend вызывает `POST /api/v1/dialogs` (если `dialog_id` ещё нет), затем отправляет `POST /api/v1/dialogs/{dialog_id}/messages` с непустым `text` (без вложения).
2. Nginx и API применяют rate limits.
3. API **синхронно** вызывает Message Safety Service (`POST /internal/safety/v1/messages/check`) — шаги текст и ссылки.
3. API вызывает Message Safety v2 (`POST /internal/safety/v2/messages/check`) — шаги text/local links.
4. Далее — общая ветка вердикта (п. 5–8 ниже).
**Файловое сообщение:**
@@ -437,7 +464,7 @@ api-backend не решает, sync или async нужна проверка в
5. **`403 deny`**: API удаляет quarantine (если был файл), выставляет `safety_status=blocked`, `delivery_status=rejected`, возвращает клиенту безопасную ошибку; в Bitrix24 ничего не уходит.
6. **`200 allow`**: API переносит файл в S3-data attachments (если был), сохраняет сообщение (`safety_status=allowed`, `delivery_status=accepted`) и фиксирует задачу доставки в Open Lines. После успешной отправки через `bitrix-local-app` статус становится `delivery_status=delivered`, API подтверждает клиенту финальный результат; `Dialog.status``waiting_for_company`. Если Bitrix24/S3/dependency недоступны после allow, статус становится `delivery_status=failed`, клиент получает безопасную ошибку зависимости.
7. **`203 pending` + `task_id`**: api-backend пишет checkpoint в `safety_tasks` и **регулярно синхронно** вызывает `GET /internal/safety/v1/messages/tasks/{task_id}` (backoff), пока не получит финальный вердикт или не истечёт `MESSAGE_SAFETY_TASK_POLL_MAX_SEC`. Пока идёт poll, **этот** клиентский `POST .../messages` ещё не завершён (соединение ждёт). Параллельные запросы других клиентов **не** блокируются — общей очереди анализа на api-backend нет.
7. **`202 pending`**: api-backend пишет checkpoint с `Location` и синхронно поллит его с `Retry-After`, пока не получит финальный вердикт/terminal failure или не истечёт budget. Public POST остаётся открытым; другие запросы не блокируются.
- финальный **`200 allow`** → как п. 6, затем ответ клиенту;
- финальный **`403 deny`** → как п. 5, затем ответ клиенту;
- timeout / недоступность safety → `delivery_status=failed`, безопасная ошибка клиенту (`503` / `504`), quarantine не promote; recovery по `safety_tasks` — зона модуля.
@@ -448,7 +475,7 @@ api-backend не решает, sync или async нужна проверка в
- Для доставки в Bitrix24 используется transactional outbox/checkpoint в App DB: запись `Message` и запись намерения доставки фиксируются атомарно, а повторная отправка в `bitrix-local-app` идемпотентна по `message_id` / `Idempotency-Key`.
- `delivery_status=accepted` означает, что API принял сообщение и завершил safety allow, но ещё не получил подтверждение доставки в Open Lines. `delivery_status=delivered` выставляется только после успешного ответа `bitrix-local-app` о приёме сообщения для Bitrix24 Open Lines.
- Recovery по `han_app.safety_tasks` восстанавливает только сценарии, где Message Safety вернул `203 pending` и клиентский запрос оборвался из-за timeout/crash. Recovery job повторно опрашивает `message-safety` по `task_id`, затем идемпотентно выполняет promote/delete quarantine и обновляет `Message`/`MessageAttachment`.
- Recovery по `han_app.safety_tasks` восстанавливает сценарии `202 pending` после timeout/crash, опрашивает сохранённый `Location`, затем идемпотентно выполняет conditional promote/delete и обновляет App DB.
- Объекты в S3-quarantine не удаляются при timeout safety до финального verdict; orphan-cleanup удаляет только просроченные объекты без активного `safety_tasks` или attachment metadata.
## Поток работы с чатом: Битрикс24 -> клиент
@@ -495,11 +522,11 @@ Notification Center v1 регистрирует переданные продю
App DB — **локальный кэш** для UI. Двусторонний sync — `bitrix-sync` (имена полей — [`arch-00-glossary.md`](arch-00-glossary.md)):
- **Auth-телефон:** master — Keycloak (`UserIdentity.phone_number`); изменения могут инициировать `contact.update` через триггеры.
- **Поля профиля для UI:** master — последнее успешно синхронизированное значение; основной входящий поток на MVP — правки сотрудником в Bitrix24 (webhook → App DB).
- **App → Bitrix:** триггеры `han_app``sync_queue` (`contact.update`).
- **Bitrix → App:** webhook робота → `bitrix-sync`; запись с GUC `han.sync_suppress` (без эхо в очередь).
- **Конфликт:** побеждает более позднее событие (`updated_at`, audit в `bitrix_sync`).
- **Auth-телефон:** master — Keycloak (`UserIdentity.phone_number`); только его фактическое изменение инициирует `contact.update`.
- **ФИО, гражданство, email:** master — Битрикс24; App хранит последний успешно полученный snapshot для UI и не отправляет эти поля обратно.
- **App → Bitrix:** триггеры `han_app``sync_queue` (`contact.map_or_create`, `contact.update`, `contact.deactivate`).
- **Bitrix → App:** durable webhook inbox + reconciliation; запись с `SET LOCAL han.sync_suppress='true'` без эхо.
- **Конфликт:** универсального правила «последнее событие побеждает» нет; применяется field mastership. Несовпадение identity/mapping создаёт business alert и не перезаписывает профиль.
## Аудит скачиваний
@@ -530,6 +557,7 @@ App DB — **локальный кэш** для UI. Двусторонний syn
## Принципы безопасности
- Все защищенные пользовательские API требуют валидный JWT.
- Компрометация одного сервиса не должна автоматически давать host root, Docker daemon, секреты или сетевой доступ соседних сервисов; требования к VM и production-деплою — в [`arch-06-service-hosting-security.md`](arch-06-service-hosting-security.md).
- Без JWT доступны **только** read-only публичные endpoint: `GET /api/v1/public/*` (rate limit + CORS + кэш). Write-endpoint (`consents`, `session-start`, чат, профиль и т.д.) требуют JWT.
- Все внешние пользовательские соединения работают через HTTPS.
- HTTP допускается только для веб-домена как вход для редиректа на HTTPS. Для api домена HTTP не допускается.
@@ -546,7 +574,7 @@ App DB — **локальный кэш** для UI. Двусторонний syn
- Все публичные id создаются в формате UUID.
- Сервисные API защищаются внутренней сетью Docker/VPC плюс service token (канонический контракт — [`arch-02-api-contracts.md`](arch-02-api-contracts.md), «Service tokens (internal API)»; значения переменных — [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md)).
- Rate limits применяются минимум на двух уровнях: edge-лимиты в `nginx` и пользовательские лимиты в API с состоянием в Redis.
- Входящие сообщения пользователя: синхронный `POST /internal/safety/v1/messages/check` → при `203` api-backend синхронно поллит `task_id` до финального `200`/`403` (или timeout); без очереди анализа на api-backend.
- Исходящие сообщения пользователя: internal `POST /internal/safety/v2/messages/check` → при `202` api-backend синхронно поллит `Location` до финального `200`/`403`, terminal failed `503` или timeout; public API не становится async.
- Файлы пользователя до финального `allow` только в S3-quarantine; в S3-data — после `200 allow`.
- У клиента **нет** постоянных S3 credentials. Загрузка — **presigned PUT** в S3-quarantine, выданный `api-backend`; скачивание — **presigned GET**. Байты файла **не** проксируются через `api-backend`.
- `message-safety` — read-only к S3-quarantine, без прав записи в бакеты.
@@ -559,13 +587,18 @@ App DB — **локальный кэш** для UI. Двусторонний syn
### Состав backend-контура
Минимальный целевой real-SMS контур на одной VM: `nginx`, `api-backend`, `message-safety`, `keycloak`, `sms-service`/worker, `bitrix-sync`, `bitrix-local-app`, `redis`, `otel-collector`. До SMS rollout сервисы SMS могут отсутствовать, но Keycloak обязан оставаться в mock mode.
Минимальный целевой real-SMS контур разделён на два stack:
- ВМ1: `nginx`, `api-backend`, `keycloak`, `sms-service`/worker, `bitrix-local-app`, Redis DB0/DB1, `otel-collector`;
- ВМ2: nginx с public webhook/private internal server blocks, `message-safety` API/worker, `clamd`/`freshclam`, `bitrix-sync`, Redis Safety, `otel-collector`.
До SMS rollout сервисы SMS могут отсутствовать, но Keycloak обязан оставаться в mock mode.
### Предлагаемая структура backend-репозитория
```text
backend/
docker-compose.yml # корневой compose: nginx + include сервисов + networks/volumes
docker-compose.yml # root compose ВМ1
.env.example
nginx/
docker-compose.yml
@@ -579,12 +612,6 @@ backend/
tests/
pyproject.toml
Dockerfile
message-safety/
app/
docker-compose.yml
tests/
pyproject.toml
Dockerfile
bitrix-local-app/
app/
docker-compose.yml
@@ -592,12 +619,6 @@ backend/
tests/
pyproject.toml
Dockerfile
bitrix-sync/
app/
docker-compose.yml
tests/
pyproject.toml
Dockerfile
keycloak/
docker-compose.yml
realm/
@@ -611,14 +632,23 @@ backend/
redis/
docker-compose.yml
observability/
docker-compose.yml # сервис otel-collector
docker-compose.yml # collector ВМ1
otel-collector.yaml
processing/
docker-compose.yml # root compose ВМ2
nginx-internal/
message-safety/
bitrix-sync/
clamav/
redis/
observability/ # collector ВМ2
```
Детальная внутренняя структура каждого сервиса (`app/`, модули, миграции) определяется в профильных спецификациях модулей (TBD).
### Compose-контур
### Compose-контуры
Корневой `backend/docker-compose.yml` подключает сервисные compose-файлы через `include`.
`backend/docker-compose.yml` является единственным root Compose ВМ1; `processing/docker-compose.yml` — единственным root Compose ВМ2. Оба используют `include` и отдельные root-owned systemd units. Cross-host Docker network не используется.
Публикация портов наружу разрешена только `nginx` (`80/443`). Остальные сервисы доступны через Docker-сети и private VPC.
На каждой VM host ports публикует только её nginx. ВМ1 публикует `80/443` своего application host. ВМ2 публикует `80/443` отдельного webhook host и private `8443`; public server block ВМ2 допускает только exact CRM webhook, private listener доступен только SG ВМ1/ops.