Реализованы сервисы ВМ2 - проверка сообщений и синхронизация с Б24 (деплой еще без перевода в боевой режим)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
bitrix_local
|
||||
@@ -318,7 +318,7 @@ COMMIT
|
||||
return stable user_id
|
||||
```
|
||||
|
||||
Повторный bootstrap безопасен: уникальный ключ согласия не создаёт дубль; `last_login_at` обновляется. Триггеры на `UserIdentity`/`ClientProfile` создают `contact.map_or_create`/`contact.update` в `sync_queue`; application code задач не вставляет.
|
||||
Повторный bootstrap безопасен: уникальный ключ согласия не создаёт дубль; `last_login_at` обновляется. Триггеры на `UserIdentity`/`ClientProfile` создают `contact.map_or_create`/`contact.update`/`contact.deactivate` в `sync_queue`; application code задач не вставляет. Полный trigger/dedup/lease contract задан в [`module-07-bitrix-sync.md`](module-07-bitrix-sync.md), §6.
|
||||
|
||||
#### `POST /api/v1/consents`
|
||||
|
||||
@@ -430,7 +430,7 @@ Success `201` возвращает финальный `MessageResponse`:
|
||||
}
|
||||
```
|
||||
|
||||
На safety deny — `422 message_blocked`; blocked message допустимо сохранять для аудита, но его текст должен храниться по политике минимизации данных (см. решение M8). В той же транзакции backend создаёт отдельную локальную `company`-реплику с безопасным бизнес-текстом для сообщения или документа; эта реплика публикуется в realtime, но не отправляется в Open Lines. На dependency failure — `503/504`; если Message уже создан, его `delivery_status=failed`.
|
||||
На safety deny — `422 message_blocked`; исходный blocked text не сохраняется: применяется M8. В той же транзакции backend создаёт отдельную локальную `company`-реплику с текстом из `text_resources.mnemonic=safety.chat.blocked`; internal `rule_id` клиенту не передаётся. Реплика публикуется как `message.new`, но не отправляется в Open Lines. На dependency failure — `503/504`; если Message уже создан, его `delivery_status=failed`.
|
||||
|
||||
### 6.7. Attachments
|
||||
|
||||
@@ -615,9 +615,9 @@ CHECK consent type: `personal_data | user_agreement | marketing`. Unique `(user_
|
||||
|
||||
### 9.5. `client_profiles`
|
||||
|
||||
`id`, `user_id` UNIQUE FK, `bitrix_contact_id NULL`, `full_name`, `citizenship`, `russian_phone`, `foreign_phone`, `email`, `source_updated_at`, common fields.
|
||||
`id`, `user_id` UNIQUE FK, `full_name`, `citizenship`, `russian_phone`, `foreign_phone`, `email`, `source_updated_at`, common fields. CRM Contact ID в App DB не хранится; canonical mapping принадлежит schema `bitrix_sync`.
|
||||
|
||||
Индексы: unique active `user_id`; partial unique `bitrix_contact_id WHERE bitrix_contact_id IS NOT NULL AND record_status='A'`; `updated_at`. PII поля не включаются в логи и generic audit payload.
|
||||
Индексы: unique active `user_id`; `updated_at`. PII поля не включаются в логи и generic audit payload.
|
||||
|
||||
### 9.6. `dialogs`
|
||||
|
||||
@@ -647,7 +647,7 @@ WHERE record_status='A';
|
||||
|
||||
### 9.7. `messages`
|
||||
|
||||
Поля: `id`, `dialog_id`, `sender_type`, `content_kind`, `text`, `safety_status`, `delivery_status`, `external_message_id NULL`, `client_idempotency_key NULL`, `occurred_at`, common fields.
|
||||
Поля: `id`, `dialog_id`, `sender_type`, `content_kind`, `text`, `safety_status`, `safety_processing_mode` (`standard | mock`), `safety_config_version` bigint, `delivery_status`, `related_message_id NULL` (self-FK), `external_message_id NULL`, `client_idempotency_key NULL`, `occurred_at`, common fields. Safety mode/config version — internal audit fields и не входят в public DTO.
|
||||
|
||||
CHECK:
|
||||
|
||||
@@ -655,24 +655,25 @@ CHECK:
|
||||
- content: `text | file`;
|
||||
- safety: `pending | allowed | blocked` (`needs_review` зарезервирован, не создаётся);
|
||||
- delivery: `accepted | processing | delivered | rejected | failed`;
|
||||
- text message: `text <> ''`;
|
||||
- text message: `text <> ''`, кроме blocked client message после M8 redaction (`text=''` допустим только при `sender_type=client AND safety_status=blocked`);
|
||||
- file message: `text = ''`;
|
||||
- company message: `safety_status='allowed'`;
|
||||
- synthetic safety company-replica: `related_message_id` указывает на blocked client message, `content_kind=text`, `delivery_status=delivered`, `external_message_id=NULL`;
|
||||
- rejected → blocked; delivered → allowed.
|
||||
|
||||
Индексы: `(dialog_id, created_at, id) WHERE record_status='A'`; `(delivery_status, updated_at)` для recovery; unique `(dialog_id, external_message_id)` where external id not null; unique `(dialog_id, client_idempotency_key)` where not null.
|
||||
Индексы: `(dialog_id, created_at, id) WHERE record_status='A'`; `(delivery_status, updated_at)` для recovery; unique `(dialog_id, external_message_id)` where external id not null; unique `(dialog_id, client_idempotency_key)` where not null; unique `(related_message_id) WHERE sender_type='company' AND related_message_id IS NOT NULL`.
|
||||
|
||||
**Решение M3:** исходящее сообщение создаётся до safety со статусами `pending/accepted`, чтобы `safety_tasks` всегда имел FK и crash checkpoint. При начале poll delivery может стать `processing`; клиенту этот промежуточный ответ не отдаётся.
|
||||
|
||||
### 9.8. `message_attachments`
|
||||
|
||||
Поля: `id`, `dialog_id`, `message_id NULL`, `owner_user_id`, `direction` (`client_upload | company_inbound`), `original_file_name`, `safe_file_name`, `mime_type`, `size_bytes`, `checksum_sha256`, `scan_status`, `storage_bucket`, `object_key`, `quarantine_object_key NULL`, `upload_expires_at`, `completed_at`, common fields.
|
||||
Поля: `id`, `dialog_id`, `message_id NULL`, `owner_user_id`, `direction` (`client_upload | company_inbound`), `original_file_name`, `safe_file_name`, `mime_type`, `size_bytes`, `checksum_sha256`, `scan_status`, `storage_bucket`, `object_key`, `quarantine_object_key NULL`, `quarantine_version_id NULL`, `quarantine_etag NULL`, `upload_expires_at`, `completed_at`, common fields.
|
||||
|
||||
Ограничения:
|
||||
|
||||
- `size_bytes > 0`;
|
||||
- SHA-256 — 64 lowercase hex;
|
||||
- scan: `pending | clean | infected | failed`;
|
||||
- scan: `pending | clean | bypassed | infected | failed`; `bypassed` допустим только для file allow с `safety_processing_mode=mock`;
|
||||
- до allow client file находится только в quarantine;
|
||||
- attachment связывается максимум с одним message;
|
||||
- для MVP у message максимум одно active attachment: unique partial `message_id`.
|
||||
@@ -691,12 +692,15 @@ Reserved MVP table: `id`, `user_id`, `name`, `mime_type`, `size_bytes`, `checksu
|
||||
- `message_id` UNIQUE;
|
||||
- `attachment_id NULL`;
|
||||
- `quarantine_object_key NULL`;
|
||||
- `quarantine_version_id NULL`, `quarantine_etag NULL`;
|
||||
- `task_location`;
|
||||
- `processing_mode`, `config_version`, `rules_version NULL`, `last_poll_http_status NULL`;
|
||||
- `status`: `polling | finalizing | completed | failed`;
|
||||
- `deadline_at`, `next_poll_at`, `attempt_count`, `last_error_code`;
|
||||
- `locked_at`, `locked_by`;
|
||||
- timestamps.
|
||||
|
||||
Индексы: `(status, next_poll_at)`, `(deadline_at)`. Worker забирает `FOR UPDATE SKIP LOCKED`. Это не очередь анализа и не заменяет `message-safety`.
|
||||
Индексы: `(status, next_poll_at)`, `(deadline_at)`. Worker забирает `FOR UPDATE SKIP LOCKED`. Это не очередь анализа и не заменяет `message-safety`. Checkpoints `completed|failed` удаляются через 7 дней; active checkpoints — только после terminal reconciliation.
|
||||
|
||||
### 9.11. `delivery_outbox`
|
||||
|
||||
@@ -747,7 +751,9 @@ Append-only: `id`, `event_type`, `actor_type`, `user_id NULL`, `ux_session_id NU
|
||||
- `app_settings` — поля и правила из arch-04;
|
||||
- `text_resources`: `id`, `mnemonic`, `locale`, `text_value`, `sort_order`, common fields; unique active `(mnemonic, locale)`;
|
||||
- `popular_questions`: `id`, `mnemonic`, `locale`, `question_text`, `sort_order`, common fields; unique active `(mnemonic, locale)`;
|
||||
- `sync_queue`, `entity_external_mapping` — shared contract с `bitrix-sync`.
|
||||
- `sync_queue` — shared contract с `bitrix-sync`: migrations и trigger-функция принадлежат App DB/module-01, runtime claim выполняет `bitrix-sync` через минимальные GRANT.
|
||||
- `bitrix_sync.entity_external_mapping` и rebind workflow принадлежат исключительно `bitrix-sync`; `api-backend` их не читает и не изменяет.
|
||||
- существующие `han_app.entity_external_mapping`, `ClientProfile.bitrix_contact_id` и partial index удаляются expand/contract migration после переноса mapping и проверки отсутствия readers.
|
||||
|
||||
## 10. Alembic и транзакции
|
||||
|
||||
@@ -842,30 +848,36 @@ validate current consents and content union
|
||||
enforce rate limits and idempotency
|
||||
for file: lock attachment, require completed/pending and checksum equality
|
||||
create Message(pending, accepted)
|
||||
call POST /internal/safety/v1/messages/check
|
||||
call POST /internal/safety/v2/messages/check
|
||||
|
||||
if 200 allow:
|
||||
finalize_allow()
|
||||
persist response.processing_mode, response.config_version
|
||||
finalize_allow(processing_mode, config_version)
|
||||
elif 403 deny:
|
||||
finalize_deny()
|
||||
elif 203 pending:
|
||||
persist safety_tasks(task_id, deadline)
|
||||
persist response.processing_mode, response.config_version
|
||||
finalize_deny(processing_mode, config_version)
|
||||
elif 202 pending:
|
||||
persist safety_tasks(task_id, Location, deadline, processing_mode=standard, config_version)
|
||||
while monotonic_now < request_deadline:
|
||||
sleep(backoff_with_jitter)
|
||||
poll GET /internal/safety/v1/messages/tasks/{task_id}
|
||||
if 200 allow: finalize_allow() and return
|
||||
if 403 deny: finalize_deny() and return
|
||||
if 400 and code=stub_final_error and verdict=deny and details.terminal=true:
|
||||
finalize_deny() and return
|
||||
poll GET Location
|
||||
if 200 allow: persist processing_mode/config_version; finalize_allow(...) and return
|
||||
if 403 deny: persist processing_mode/config_version; finalize_deny(...) and return
|
||||
if 503 and terminal=true and retryable=false:
|
||||
mark failed and return 503
|
||||
if other 4xx/5xx: return mapped dependency error
|
||||
mark failed, retain checkpoint/quarantine
|
||||
return 504
|
||||
elif 409 and code=safety_request_conflict:
|
||||
alert invariant violation, mark failed, return 500, do not repeat POST
|
||||
else:
|
||||
mark failed
|
||||
return mapped dependency error
|
||||
```
|
||||
|
||||
Polling interval начинается с `MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC`, допускает capped exponential backoff и jitter, но не превышает общий `MESSAGE_SAFETY_TASK_POLL_MAX_SEC`. Клиенту не возвращается `203`. Terminal `400 stub_final_error` — намеренное test-only расширение `module-05`; оно преобразуется в публичный `422 message_blocked`, не считается infrastructure failure и не смешивается с malformed `400`.
|
||||
Polling interval начинается с server `Retry-After`, допускает capped exponential backoff и jitter, но не превышает caller env `MESSAGE_SAFETY_TASK_POLL_MAX_SEC`. Каждый v2 verdict/pending содержит `processing_mode` и `config_version`; MOCK возвращает только sync `200/403`. Клиенту internal mode/config/`202` не возвращаются: public POST сохраняет синхронную семантику. Legacy stub `/v1` с `203`/`stub_final_error` поддерживается только временным adapter-ом до cutover и не является target production path.
|
||||
|
||||
Capability snapshot `/health/ready` допускается кэшировать не дольше 5 с для fast-fail: file требует `files`, text с URL — `links`, text без URL — `text`. При `processing_mode=mock` normal capabilities имеют состояние `bypassed` и не применяются как fast-fail gate. Snapshot не является correctness gate: definitive capability повторно проверяет `POST /check`. При unavailable в standard mode api-backend возвращает public `503 dependency_unavailable`, не создаёт delivery outbox и не меняет status на blocked.
|
||||
|
||||
### 13.2. Final allow
|
||||
|
||||
@@ -874,13 +886,20 @@ Polling interval начинается с `MESSAGE_SAFETY_TASK_POLL_INTERVAL_SEC`
|
||||
1. проверить checkpoint;
|
||||
2. copy quarantine object в attachments bucket с conditional/idempotent key;
|
||||
3. HeadObject destination, сверить checksum/size;
|
||||
4. в транзакции изменить attachment на `clean`, storage location на S3-data; message на `allowed/accepted`; создать delivery outbox;
|
||||
4. в транзакции изменить attachment на `clean` для standard mode или `bypassed` для MOCK, storage location на S3-data; message на `allowed/accepted` с фактическим `safety_processing_mode`; создать delivery outbox;
|
||||
5. удалить quarantine object best-effort; при сбое cleanup повторит;
|
||||
6. попытаться синхронно доставить outbox, чтобы исходный POST вернул финальный `delivered`.
|
||||
|
||||
### 13.3. Final deny
|
||||
|
||||
В транзакции: `blocked/rejected`, attachment `infected`; затем delete quarantine best-effort. В Open Lines ничего не отправляется. Realtime `message.status` публикуется, если message уже мог быть виден этому клиенту.
|
||||
В одной транзакции:
|
||||
|
||||
1. client message → `blocked/rejected`, `text` заменяется пустой строкой/безопасным marker по M8;
|
||||
2. attachment при наличии → `infected`;
|
||||
3. создаётся ровно одна synthetic company-replica, связанная `related_message_id`, с `text` из active `text_resources(safety.chat.blocked, locale)`, `allowed/delivered`;
|
||||
4. сохраняются только hash, internal rule/verdict/version в audit.
|
||||
|
||||
После commit quarantine удаляется best-effort. В Open Lines ничего не отправляется. Realtime публикует `message.status` исходного сообщения и `message.new` company-реплики. Public `422` содержит generic error envelope без `rule_id`.
|
||||
|
||||
### 13.4. Timeout/crash recovery
|
||||
|
||||
@@ -892,13 +911,13 @@ claim with lease
|
||||
poll safety by task_id
|
||||
if pending before recovery deadline: schedule next_poll_at
|
||||
if allow: idempotent promote + delivery checkpoint
|
||||
if deny (canonical 403 or test-only terminal 400): idempotent delete + reject
|
||||
if deny (canonical 403): idempotent delete + reject + company replica
|
||||
if budget exhausted: mark task failed, message failed, preserve audit
|
||||
```
|
||||
|
||||
HTTP disconnect не отменяет durable recovery. Клиентский retry с тем же idempotency key получает восстановленный результат либо текущую dependency error. Recovery не принимает решение о типе анализа.
|
||||
|
||||
**Решение M5:** после client-facing timeout recovery budget продолжается ещё 15 минут как техническая константа модуля; до production-load test значение должно быть вынесено в infra env и добавлено в arch-04. Пока это **TBD-2**, код обязан иметь безопасный default и метрику.
|
||||
**Решение M5:** `deadline_at = min(message_safety.expires_at, checkpoint.created_at + HAN_APP_SAFETY_RECOVERY_MAX_SEC)`, initial env = 1200 с. Client-facing wait остаётся 300 с; recovery продолжает без открытого клиентского соединения. Terminal Safety `503 retryable=false` немедленно завершает checkpoint как failed.
|
||||
|
||||
## 14. S3 attachment lifecycle
|
||||
|
||||
@@ -914,23 +933,23 @@ Lifecycle:
|
||||
|
||||
1. `init`: allow-list extension + declared MIME + size; create metadata; presign exact key, MIME, max size, TTL;
|
||||
2. direct PUT client → S3-quarantine;
|
||||
3. `complete`: HeadObject, size/MIME/checksum metadata; checksum при отсутствии trustworthy S3 checksum вычисляется safety service при scan;
|
||||
3. `complete`: HeadObject конкретной version, size/MIME/server checksum; атомарно фиксирует `version_id + ETag + authoritative checksum`;
|
||||
4. message send: attachment ownership/state/checksum;
|
||||
5. allow: copy + verify + DB finalize + quarantine delete;
|
||||
5. allow: conditional copy сохранённой source version с ETag/checksum match + verify + DB finalize + quarantine delete;
|
||||
6. deny: quarantine delete + infected metadata;
|
||||
7. abandoned/failed: cleanup only if expired and no active safety task;
|
||||
7. abandoned/failed: cleanup через 48 ч, только если нет active safety task;
|
||||
8. download: owner check → audit commit → short presigned GET.
|
||||
|
||||
Extension и MIME оба должны быть разрешены; server normalizes filename and sets safe `Content-Disposition`. S3 credentials never reach frontend.
|
||||
|
||||
**Допущение A3:** Selectel S3 может не предоставлять SHA-256 в `HeadObject`; `complete` сверяет клиентский checksum с signed metadata, а authoritative checksum подтверждает Message Safety. Если storage поддерживает checksum header, он обязателен.
|
||||
Presigned PUT обязательно подписывает `If-None-Match: *`, checksum header и `Content-Type`; versioning quarantine включён. Повторный PUT того же key получает `412`. При отсутствии подтверждённой поддержки этих условий выбранным S3 adapter production upload блокируется, а не деградирует до overwrite.
|
||||
|
||||
Inbound operator file:
|
||||
|
||||
- validate count/size/MIME and URL scheme/host policy;
|
||||
- protect against SSRF: no redirects to private/link-local ranges, DNS rebinding checks, max bytes streaming;
|
||||
- download with timeout to temporary stream, never local persistent disk;
|
||||
- optional antivirus policy; Message Safety outbound pipeline не вызывается;
|
||||
- Message Safety/ClamAV не вызываются; остаточный malware-риск доверенного Bitrix24-channel принят для MVP;
|
||||
- upload directly to S3-data attachments;
|
||||
- only then atomically save attachment/message and ack inbox.
|
||||
|
||||
@@ -973,13 +992,19 @@ Outbox worker и synchronous first attempt используют один dispatc
|
||||
|
||||
Миграции создают triggers:
|
||||
|
||||
- insert active `UserIdentity`/`ClientProfile` без mapping → `contact.map_or_create`;
|
||||
- изменение tracked profile/auth-phone fields → `contact.update`;
|
||||
- trigger строит deterministic dedup key;
|
||||
- insert active `UserIdentity`/`ClientProfile` → coalesced `contact.map_or_create`; trigger не читает schema `bitrix_sync`, наличие mapping проверяет worker;
|
||||
- фактическое изменение App-master `UserIdentity.phone_number` → `contact.update`;
|
||||
- переход `UserIdentity` или `ClientProfile` из active в inactive/deleted → `contact.deactivate`;
|
||||
- возврат active записи → coalesced `contact.map_or_create`;
|
||||
- изменения CRM-master `full_name`, `citizenship`, `email` не создают App→CRM задачу;
|
||||
- trigger проверяет значения через `IS DISTINCT FROM`, а не только факт присутствия колонки в `UPDATE OF`;
|
||||
- trigger строит deterministic dedup key, уникальный только среди активных queue rows; завершённая/cancelled/dead-letter запись не блокирует новое событие;
|
||||
- при `current_setting('han.sync_suppress', true)='true'` задача не создаётся;
|
||||
- trigger и business update находятся в одной транзакции.
|
||||
|
||||
`bitrix-sync` получает ограниченные GRANT. Ошибка CRM не откатывает bootstrap и chat. Open Lines не зависит от CRM mapping.
|
||||
`entity_id` всех contact-задач — `UserIdentity.id`; payload содержит только `schema_version`, `user_id`, безопасную причину и source timestamp, но не PII snapshot. Worker перечитывает актуальные identity/profile.
|
||||
|
||||
`bitrix-sync` получает ограниченные column/table GRANT, заданные module-07. Ошибка CRM не откатывает bootstrap и chat. Open Lines не зависит от CRM mapping.
|
||||
|
||||
## 17. Realtime
|
||||
|
||||
@@ -1197,6 +1222,8 @@ Open Lines недоступность отображается как component
|
||||
|
||||
**Решение M8:** blocked message text не нужен продукту после deny. В `messages.text` хранится пустая строка/безопасный redacted marker, а audit хранит только rule/verdict id и hash содержимого. Если регуляторно требуется исходный текст, это отдельное согласованное изменение retention/security.
|
||||
|
||||
Оценка monitor-only semantic rules выполняется контролируемой, аудируемой выборкой из App DB: доступ только у утверждённой роли, выборка ограничена по времени/объёму, purpose фиксируется в audit. Текст не копируется в schema/логи Message Safety; там остаются hash, `rule_id` и version.
|
||||
|
||||
## 24. Docker/runtime
|
||||
|
||||
Service compose:
|
||||
@@ -1205,7 +1232,7 @@ Service compose:
|
||||
- networks: `backend`, `observability`;
|
||||
- env только через `${VAR}` из root `.env`;
|
||||
- healthcheck `/health/live` для процесса; root orchestration учитывает readiness;
|
||||
- depends_on health для Redis/Keycloak/message-safety, но приложение само retry startup dependencies;
|
||||
- depends_on health только для локальных Redis/Keycloak; remote Message Safety не является Compose dependency и проверяется capability-aware на send path;
|
||||
- managed PostgreSQL вне compose, TLS обязателен;
|
||||
- stateless container, без persistent volume;
|
||||
- init process для signal forwarding;
|
||||
@@ -1251,7 +1278,7 @@ Frontend выполняет refresh token grant. API не обновляет tok
|
||||
|
||||
### 25.3. Файловое сообщение
|
||||
|
||||
create/reuse dialog → init → direct PUT quarantine → complete → send file message → safety `203` poll → allow promote → delivery outbox → Open Lines → delivered. При deny quarantine удаляется, Bitrix не вызывается.
|
||||
create/reuse dialog → init → direct immutable PUT quarantine → complete with version/ETag/checksum → send file message → safety `202` poll → allow conditional promote → delivery outbox → Open Lines → delivered. При deny quarantine удаляется, Bitrix не вызывается.
|
||||
|
||||
### 25.4. Ответ оператора
|
||||
|
||||
@@ -1289,7 +1316,7 @@ Bitrix event → local app durable inbox → `POST /internal/openlines/v1/inbox`
|
||||
### 26.3. Contract
|
||||
|
||||
- generated FastAPI OpenAPI matches committed `api-backend/openapi.yaml`;
|
||||
- Message Safety POST `200/203/403`, task poll `203/200/403` и test-only terminal `400 stub_final_error`; проверены различение malformed `400` и mapping terminal `400` → public `422`;
|
||||
- Message Safety v2 POST `200/202/403`, task poll `202/200/403`, terminal failed `503` и conflict `409`; legacy stub adapter тестируется отдельно до cutover;
|
||||
- Open Lines message idempotency and inbox schemas;
|
||||
- settings bridge DTO/token;
|
||||
- common request-id/trace propagation;
|
||||
@@ -1368,15 +1395,15 @@ Bitrix event → local app durable inbox → `POST /internal/openlines/v1/inbox`
|
||||
|
||||
- **A1:** locale API зарезервирован, MVP фактически `ru`.
|
||||
- **A2:** inbox получит стабильный `event_id`; временно возможен deterministic fingerprint.
|
||||
- **A3:** authoritative SHA-256 может подтверждаться Message Safety, если S3 HeadObject его не отдаёт.
|
||||
- **A3:** production S3 adapter подтверждает signed checksum headers, versioning и conditional requests; иначе immutable upload не включается.
|
||||
|
||||
### Требуют согласования
|
||||
|
||||
- **TBD-1:** единый код для protected endpoint до bootstrap (`409` предложен).
|
||||
- **TBD-2:** extended recovery budget после `MESSAGE_SAFETY_TASK_POLL_MAX_SEC`.
|
||||
- **Решение M5:** extended recovery ограничен `HAN_APP_SAFETY_RECOVERY_MAX_SEC=1200` и Safety `expires_at`.
|
||||
- **TBD-3:** добавить `event_id`/`occurred_at` в WS events и `event_id` в inbox OpenAPI.
|
||||
- **TBD-4:** production SLO, RPS, concurrency, RPO/RTO и retention.
|
||||
- **TBD-5:** antivirus policy для файлов оператора, которые не проходят outbound Message Safety.
|
||||
- **Решение M9:** файлы оператора не проходят Message Safety/AV в MVP; только MIME/size/audit, residual malware risk принят.
|
||||
- **TBD-6:** legal retention/erasure для PII, audit, blocked messages и S3-data.
|
||||
- **TBD-7:** точный max WS connections/subscriptions/frame и queue size.
|
||||
- **TBD-8:** G10 — окончательный DTO/mapping public app-config при оформлении OpenAPI.
|
||||
|
||||
@@ -90,7 +90,7 @@ Resend запускает новое Keycloak action, блокирует double
|
||||
- сообщения сортируются по `created_at asc`, дубли объединяются по `message_id`;
|
||||
- `waiting_for_company`, `waiting_for_client`, `closed` отображаются русскими подписями;
|
||||
- завершённая беседа readonly; CTA «Продолжить общение» повторно открывает текущий чат через `POST /dialogs`;
|
||||
- промежуточный safety `203` клиенту не показывается: send request остаётся в progress до финального ответа.
|
||||
- internal safety `202` клиенту не показывается: send request остаётся in progress до финального ответа.
|
||||
|
||||
### 5.4. Профиль
|
||||
|
||||
@@ -146,7 +146,7 @@ Bootstrap повторяем безопасно после неопределё
|
||||
3. `POST /dialogs` с idempotency key, сохранить `dialog_id` и перейти на экран чата до отправки.
|
||||
4. Экран чата выполняет `POST /dialogs/{id}/messages` с отдельным стабильным key.
|
||||
5. Блокировать повторный click только для того же intent; другие действия не замораживать.
|
||||
6. На `201` merge `MessageResponse`; на `422 message_blocked` не показывать красную техническую ошибку, а обновить историю с сохранённой backend `company`-репликой; на `503/504` предложить retry с тем же key.
|
||||
6. На `201` merge `MessageResponse`; на `422 message_blocked` не показывать internal reason/rule и не строить собственный текст: получить/merge backend `company`-реплику (`message.new`) с контентом мнемоники `safety.chat.blocked`; на `503/504` предложить retry с тем же key.
|
||||
7. Composer показывает счётчик `n/max`, где `max` приходит как `messages.max_text_length` из public app-config; сверх лимита отправка блокируется без обрезки ввода.
|
||||
|
||||
## 11. Файловый flow
|
||||
@@ -156,11 +156,11 @@ MVP допускает ровно один файл, только allow-list ext
|
||||
1. Локальная prevalidation.
|
||||
2. Создать/reuse dialog.
|
||||
3. `POST .../attachments/init` с filename, MIME, size.
|
||||
4. Выполнить прямой `PUT upload_url` с точно выданными `upload_headers`; API domain при этом не используется.
|
||||
5. Вычислить SHA-256, вызвать `complete`.
|
||||
4. Вычислить SHA-256 до upload и выполнить прямой `PUT upload_url` с **точно** выданными `upload_headers`, включая `If-None-Match: *` и checksum; заголовки являются частью подписи.
|
||||
5. Вызвать `complete` с тем же checksum; backend фиксирует authoritative S3 version/ETag/checksum.
|
||||
6. Отправить file message с `attachment_id` и `sha256:<hex>`.
|
||||
|
||||
Presigned URL не сохраняется и редактируется из диагностик. Abort позволяет отменить PUT; orphan очищает backend. При expiry init выполняется повторно в рамках согласованного состояния. CORS S3 должен разрешать origin сайта, PUT и необходимые headers.
|
||||
Presigned URL не сохраняется и редактируется из диагностик. `412` означает, что immutable key уже записан: frontend не повторяет PUT в тот же key, а запрашивает новый init. Abort позволяет отменить PUT; orphan очищает backend через 48 ч. При expiry init выполняется повторно в рамках согласованного состояния. CORS S3 разрешает origin сайта, PUT и только необходимые signed headers.
|
||||
|
||||
## 12. Realtime и polling
|
||||
|
||||
@@ -292,4 +292,4 @@ Browsers: Chromium, Firefox, WebKit; viewport desktop/mobile. В CI исполь
|
||||
- F3: web storage policy refresh token перед production security review;
|
||||
- F4: окончательный DTO app-config (G10);
|
||||
- F5: WS `event_id`/protocol version (TBD module-01/G11);
|
||||
- F6: продуктовые тексты всех error states по мнемоникам.
|
||||
- F6 для Safety закрыт: generic deny использует `safety.chat.blocked`; остальные error-state мнемоники остаются в frontend content backlog.
|
||||
|
||||
+48
-14
@@ -1,39 +1,68 @@
|
||||
# module-03. Проектная спецификация корневого `nginx`
|
||||
|
||||
> Статус: целевая спецификация полностью рабочего edge-контура MVP.
|
||||
> Статус: целевая спецификация независимых nginx-контуров ВМ1 и ВМ2.
|
||||
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md).
|
||||
|
||||
## 1. Назначение и обязательная топология
|
||||
|
||||
В production-like контуре существует ровно один корневой nginx. Только он публикует host-порты `80/443`, завершает TLS, раздаёт SPA и проксирует публичные маршруты. Контейнеры API, Keycloak, Redis, Safety, Bitrix и OTEL используют только `expose`/Docker networks.
|
||||
В production-like контуре каждая VM имеет собственный nginx в своём root Compose и собственный deployment lifecycle:
|
||||
|
||||
Если перед VM есть внешний WAF/LB, доверенные proxy CIDR задаются явно; nginx не доверяет произвольному `X-Forwarded-For`. Другой nginx на host не должен маршрутизировать сервисы по отдельности.
|
||||
- nginx ВМ1 обслуживает frontend/API/auth/Open Lines/SMS;
|
||||
- nginx ВМ2 напрямую обслуживает публичные CRM webhook `bitrix-sync` и private Message Safety API;
|
||||
- публичный трафик ВМ2 не проходит через ВМ1;
|
||||
- отказ или deploy ВМ1 не прерывает приём CRM webhook на ВМ2.
|
||||
|
||||
Контейнеры приложений не публикуют host ports. На каждой VM наружу смотрит только её nginx. Если перед конкретной VM есть WAF/LB, trusted proxy CIDR задаются отдельно.
|
||||
|
||||
## 2. Routing matrix
|
||||
|
||||
Порядок location критичен: exact/longest public routes до общего `/bitrix/`.
|
||||
Порядок location критичен. Публичные route разделены по host/VM.
|
||||
|
||||
### ВМ1
|
||||
|
||||
| Внешний путь | Upstream | Режим |
|
||||
|---|---|---|
|
||||
| `/api/` | `api-backend:8000` | REST; `/api/v1/realtime` WS |
|
||||
| `/auth/` | `keycloak:8080` | OIDC/OTP, prefix/hostname согласован с issuer |
|
||||
| exact `/callbacks/idgtl/sms` | `sms-service:8080` | public HTTPS POST Direct; IP allowlist + Basic auth в upstream |
|
||||
| `/bitrix/sync/webhook/contact` | `bitrix-sync:8080` | public HTTPS POST, no cache; отсутствует, пока действует stub module-07 |
|
||||
| `/bitrix/handler`, `/bitrix/install`, `/bitrix/placement` | `bitrix-local-app:8080` | public HTTPS |
|
||||
| exact `/health/live`, `/health/ready` | `bitrix-local-app:8080` | по умолчанию не публикуются; только при явно выбранной ops/monitoring policy |
|
||||
| `/` | static SPA либо Expo dev upstream | `try_files` fallback |
|
||||
|
||||
Notification paths внутри `/api/` имеют отдельные edge-зоны: public catalog/campaigns, JWT read, actions, uploads и downloads. `/internal/`, `/_internal/`, Redis/OTLP/admin/status/config files запрещены exact prefix response `404` (допустим `403`, но единообразно выбран `404`). Никакого fallback internal path в SPA или общий proxy. `message-safety`, `/internal/sms/*` и `/internal/notifications/*` не имеют публичного route.
|
||||
### ВМ2
|
||||
|
||||
| Внешний путь | Upstream | Режим |
|
||||
|---|---|---|
|
||||
| exact `/bitrix/sync/webhook/contact` | `bitrix-sync:8080` | public HTTPS Contact event; source IP CIDR/method/body/rate limits, query-token auth в upstream |
|
||||
| exact `/bitrix/sync/webhook/alert` | `bitrix-sync:8080` | public HTTPS smart-process event; те же ограничения |
|
||||
|
||||
Notification paths внутри `/api/` ВМ1 имеют отдельные edge-зоны. На обоих public hosts `/internal/`, `/_internal/`, Redis/OTLP/admin/status/config files возвращают `404`; fallback на другую VM или SPA запрещён. До full sync cutover оба exact webhook route ВМ2 закрыты либо возвращают retryable `503`; успешный `2xx ignored` запрещён.
|
||||
|
||||
Query не участвует в exact location matching: URL штатного робота `/bitrix/sync/webhook/<type>?token=...&ID=...` попадает в соответствующий exact route. До proxy nginx проверяет непосредственный source IP по version-controlled `BITRIX_WEBHOOK_ALLOWED_CIDRS`; пустой/невалидный список при enabled receiver блокирует deployment. Адрес из недоверенного `X-Forwarded-For` не используется. При внешнем LB сначала настраиваются его trusted CIDR и нормализация real IP.
|
||||
|
||||
Запрос вне allow-list получает generic `403` без proxy. В безопасном журнале с ограниченным retention сохраняются только timestamp, source IP, route class и outcome; query/body не сохраняются. Telemetry pipeline экспортирует `webhook_rejected_total{receiver,reason="source_ip"}` без IP label. Allow-list не расширяется автоматически: всплеск Contact, восстановленных инкрементальной reconciliation, инициирует проверку rejected-IP журнала, подтверждение принадлежности адреса Битрикс24 и reviewed reload конфигурации.
|
||||
|
||||
## 3. Upstreams
|
||||
|
||||
Именованные upstream: `api_backend`, `keycloak`, `sms_service`, `bitrix_local`, `bitrix_sync`, опционально `frontend_dev`. Для одной replica допустим `server service:port`; `keepalive` включён. Docker DNS resolver задаётся с коротким `valid` и `resolve` там, где поддерживает выбранная nginx edition; иначе контейнер перезапускается при смене IP upstream.
|
||||
Именованные upstream ВМ1: `api_backend`, `keycloak`, `sms_service`, `bitrix_local`, опционально `frontend_dev`, а также private `processing_gateway` только для вызовов Message Safety из api-backend.
|
||||
|
||||
Nginx ВМ2 имеет независимые server blocks:
|
||||
|
||||
- public `80/443` на отдельном DNS host: ACME/redirect и два exact CRM webhook;
|
||||
- private `8443` с сертификатом internal CA: только server-to-server Message Safety и approved ops.
|
||||
|
||||
| Path | Local upstream | Caller |
|
||||
|---|---|---|
|
||||
| `/internal/safety/v2/*` | `message-safety-api:8080` | api-backend ВМ1 |
|
||||
| `/internal/sync/v1/*` | `bitrix-sync:8080` | ops/allow-listed service |
|
||||
|
||||
Public и private server blocks не имеют общего fallback. Все прочие paths/methods возвращают `404/405`. Private listener доверяет forwarded headers только от allow-listed private caller; public listener применяет собственную trusted proxy policy.
|
||||
|
||||
Upstream failures не перенаправляются на другой сервис и не попадают в SPA. API возвращает `502/504` с безопасным nginx body и `X-Request-ID`; custom JSON error допустим для `/api`, но не имитирует backend domain code.
|
||||
|
||||
## 4. HTTP/HTTPS и TLS
|
||||
|
||||
- единый web host `:80` обслуживает только ACME challenge и `308 https://$host$request_uri`;
|
||||
- public host каждой VM на `:80` обслуживает только ACME challenge и `308 https://$host$request_uri`; исключение — `/bitrix/sync/webhook/contact|alert`, которые возвращают generic `404/426` без redirect и отражения query token;
|
||||
- выделенный API host, если появится, не имеет listener `:80`;
|
||||
- `:443 ssl http2`, TLS 1.2/1.3, современные cipher suites, session tickets по ops policy;
|
||||
- сертификат доверенного CA, private key read-only и недоступен приложению;
|
||||
@@ -134,7 +163,7 @@ traceparent: входной валидный либо новый согласн
|
||||
- `ws_connect`: handshake;
|
||||
- `connections`: `limit_conn`.
|
||||
|
||||
Ответ превышения — `429`, `Retry-After` (статический/вычисляемый для зоны) и request id. Edge не реализует user-level бизнес-лимит; это делает API/Redis. OPTIONS не должен расходовать auth budget чрезмерно. Bitrix webhook retries имеют отдельный достаточный burst и всё равно проверяют application token в сервисе.
|
||||
Ответ превышения — `429`, `Retry-After` (статический/вычисляемый для зоны) и request id. Edge не реализует user-level бизнес-лимит; это делает API/Redis. OPTIONS не должен расходовать auth budget чрезмерно. Bitrix webhook retries имеют отдельный достаточный burst, проходят source IP allow-list и проверяют query receiver token в сервисе.
|
||||
|
||||
## 10. Static SPA и dev mode
|
||||
|
||||
@@ -195,7 +224,7 @@ Bitrix placement может требовать embedding: для exact `/bitrix/
|
||||
|
||||
## 14. Логи и OTEL correlation
|
||||
|
||||
JSON access log: timestamp, request_id, trace_id (если извлечён), remote IP/hashed policy, host, method, route class, normalized URI без sensitive query, status, bytes, duration, upstream addr/status/time, cache status, TLS protocol/cipher, user agent при принятой retention.
|
||||
JSON access log: timestamp, request_id, trace_id (если извлечён), remote IP/hashed policy, host, method, route class, normalized URI на основе `$uri` без `$request_uri`/`$args`, status, bytes, duration, upstream addr/status/time, cache status, TLS protocol/cipher, user agent при принятой retention.
|
||||
|
||||
Не логируются Authorization, cookies, request/response body, OTP, tokens, query token, presigned query, PII. Error log структурирован настолько, насколько позволяет nginx; debug выключен production.
|
||||
|
||||
@@ -253,7 +282,7 @@ docker compose exec nginx nginx -t
|
||||
curl -I http://tohin.ru/
|
||||
curl -vk https://tohin.ru/api/v1/public/app-config
|
||||
openssl s_client -connect tohin.ru:443 -servername tohin.ru
|
||||
curl -i https://tohin.ru/internal/safety/v1/messages/check
|
||||
curl -i https://tohin.ru/internal/safety/v2/messages/check
|
||||
```
|
||||
|
||||
Автоматические тесты:
|
||||
@@ -271,15 +300,20 @@ curl -i https://tohin.ru/internal/safety/v1/messages/check
|
||||
- CSP/CORS preflight и Bitrix placement exception;
|
||||
- upstream down/timeout, failed reload, renewal rehearsal;
|
||||
- logs не содержат secrets/query tokens.
|
||||
- CRM webhook exact routes принимают query без изменения location matching; allowed source IP проксируется, wrong IP получает `403` до upstream;
|
||||
- HTTP webhook URL с query token не перенаправляется на HTTPS и не отражает query в `Location`/error;
|
||||
- source-IP rejects попадают в безопасный bounded-retention журнал и low-cardinality telemetry без query/body/IP label;
|
||||
- allowed Direct callback проходит; wrong IP/method и любой `/internal/sms/*` отклоняются; Authorization отсутствует в логах.
|
||||
- `/internal/notifications/*` снаружи всегда `404`; notification read/action/upload/public routes используют свои зоны и возвращают `429`.
|
||||
- CSP содержит `frame-src 'none'`; инструкция проверяется как новая вкладка без embedded content.
|
||||
|
||||
## 19. Definition of Done
|
||||
|
||||
- единственный root nginx публикует только 80/443;
|
||||
- на каждой VM ровно один nginx; ВМ1 и ВМ2 независимо публикуют только свои утверждённые `80/443`, ВМ2 дополнительно слушает private `8443`;
|
||||
- TLS/ACME bootstrap, renewal и safe reload испытаны;
|
||||
- routing matrix и route precedence покрыты;
|
||||
- CRM webhook достигает ВМ2 напрямую и продолжает приниматься при остановленном nginx ВМ1;
|
||||
- CRM webhook ограничен version-controlled source IP CIDR allow-list; query token и form body отсутствуют в access/error logs и traces;
|
||||
- internal endpoints/ports извне недоступны;
|
||||
- WS работает на `/api/v1/realtime`;
|
||||
- message timeout равен safety max + минимум 30 секунд;
|
||||
@@ -293,8 +327,8 @@ curl -i https://tohin.ru/internal/safety/v1/messages/check
|
||||
|
||||
## 20. Решения, допущения и TBD
|
||||
|
||||
**Решения:** один nginx; njs/module для UUID; internal → 404; webroot ACME; отдельная CSP для Bitrix placement; public cache только allow-listed endpoints.
|
||||
**Решения:** независимые public nginx ВМ1/ВМ2; CRM webhook приходит прямо на ВМ2 через source IP CIDR allow-list; private `8443` ВМ2 используется только server-to-server; njs/module для UUID; public internal paths → 404; webroot ACME; отдельная CSP для Bitrix placement; public cache только allow-listed endpoints.
|
||||
|
||||
**Допущения:** MVP использует единый host `tohin.ru`; upstream service names стабильны в Compose; S3 CORS настраивается отдельно.
|
||||
**Допущения:** ВМ1 и ВМ2 используют разные public hosts и сертификаты; upstream service names стабильны внутри каждого Compose; S3 CORS настраивается отдельно.
|
||||
|
||||
**TBD:** N1 доверенные WAF CIDR; N2 production cipher suite/OCSP; N3 нужен ли публичный health; N4 точный CSP Expo build; N5 Bitrix frame ancestor domains; N6 финальные burst/connection limits; N7 certbot vs другой ACME client после ops review.
|
||||
|
||||
+31
-26
@@ -1,15 +1,15 @@
|
||||
# module-04. Проектная спецификация Redis
|
||||
|
||||
> Статус: целевая спецификация Redis в едином Docker Compose MVP.
|
||||
> Статус: целевая спецификация Redis для двух Compose-контуров; legacy DB2 stub описан только до cutover.
|
||||
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md).
|
||||
|
||||
## 1. Назначение и инварианты
|
||||
|
||||
Один Redis-контейнер предоставляет быстрые ephemeral функции трём логическим DB:
|
||||
Redis разделён по deployment/security boundary:
|
||||
|
||||
- DB0 — `api-backend`: idempotency fast layer и API rate limits;
|
||||
- DB1 — realtime и coordination;
|
||||
- DB2 — `message-safety` stub tasks/cache.
|
||||
- Redis ВМ1: DB0 (`api-backend` idempotency/rate) и DB1 (realtime/coordination);
|
||||
- Redis Safety ВМ2: отдельный instance для hot cache, rate limiting и optional worker wake-up;
|
||||
- legacy DB2 ВМ1 существует только для test stub v1 до cutover и после него удаляется.
|
||||
|
||||
Redis не является бизнес-очередью, source of truth сообщений, sync tasks, audit, профилей или delivery checkpoint. Надёжные состояния остаются в managed PostgreSQL/S3. Потеря Redis может ухудшить сервис, но не должна создавать потерю подтверждённых сообщений либо дубль side effect: durable idempotency/outbox/checkpoint api-backend описаны в module-01.
|
||||
|
||||
@@ -17,9 +17,9 @@ OTP counters api-backend в Redis не хранит; они принадлежа
|
||||
|
||||
## 2. Версия и topology
|
||||
|
||||
Redis 7.x, image закреплён по digest. Одна primary instance на VM без replica/Sentinel в MVP. Клиенты используют connection pool, bounded timeouts и не выполняют опасные команды.
|
||||
Redis 7.x, image закреплён по digest. На каждой VM одна нужная primary instance без replica/Sentinel в MVP. Клиенты используют connection pool, bounded timeouts и не выполняют опасные команды.
|
||||
|
||||
Logical DB — изоляция имён, не security boundary и не независимый memory quota. При росте или разных eviction/SLA DB2 и DB0 выносятся в отдельные instances.
|
||||
Logical DB — изоляция имён, не security boundary. Safety уже вынесен в отдельный instance ВМ2; DB0/DB1 остаются на ВМ1.
|
||||
|
||||
## 3. Общие правила ключей
|
||||
|
||||
@@ -81,18 +81,20 @@ Redis Streams не используются как бизнес queue. Если
|
||||
|
||||
Acquire: `SET key owner NX PX ttl`; extend/release — Lua compare owner. Worker обязан опираться также на PostgreSQL row lease/`FOR UPDATE SKIP LOCKED`; Redis lock — оптимизация, не единственная защита. Fencing token рекомендуется для внешнего side effect, а уникальные DB constraints/idempotency остаются финальной защитой.
|
||||
|
||||
## 8. DB2: Message Safety stub
|
||||
## 8. Redis Safety ВМ2
|
||||
|
||||
| Key | Тип/value | TTL |
|
||||
|---|---|---|
|
||||
| `han:safety:task:{task_id}` | HASH/JSON v1: created, polls, optional seed/context | `MESSAGE_SAFETY_TASK_TTL_SEC` |
|
||||
| `han:safety:tasklock:{task_id}` | owner token | 5–30s |
|
||||
| `han:safety:rl:service:{caller}:{window}` | counter | window+jitter |
|
||||
| `han:safety:verdict:{content_hash}:{rules_version}` | optional cache | bounded technical TTL |
|
||||
| `han:safety:text:{analysis_hash}:{rules_version}` | hot text-rules result, monitor rule ids без raw text | active config, seed ≤48h |
|
||||
| `han:safety:verdict:{content_hash}:{config_version}:{detector_bundle}` | hot file verdict cache | active config, seed ≤30d |
|
||||
| `han:safety:link:{url_hash}:{rules_version}:{config_version}` | stable local policy cache | active config, seed ≤48h |
|
||||
| `han:safety:dns:{host_hash}:{rrtype}` | DNS answer; classification повторяется под текущей policy | actual TTL, active hard max seed 900s |
|
||||
| `han:safety:wakeup` | Pub/Sub notification only | no storage |
|
||||
|
||||
Для требуемой заглушки task — ephemeral contract state. Истечение task возвращает безопасный `404 task_not_found/expired` по internal error semantics. В production safety authoritative audit/cache может находиться в PostgreSQL `message_safety`; Redis DB2 не заменяет его.
|
||||
PostgreSQL `message_safety.safety_tasks` — единственный queue/lease source (`FOR UPDATE SKIP LOCKED`, fencing generation). Redis не хранит authoritative task state, locks или leases. Cache loss/restart безопасно восстанавливается из PostgreSQL; Redis outage не выключает core Safety.
|
||||
|
||||
Random verdict каждого GET по заданию независим; Redis хранит существование/TTL и счётчик polls для observability, но не предопределяет финал. В deterministic tests seed/RNG injected на уровне сервиса.
|
||||
Legacy v1 stub может временно использовать DB2 ВМ1 для random task state. Этот namespace не используется production v2 и удаляется вместе со stub.
|
||||
|
||||
## 9. Serialization и limits
|
||||
|
||||
@@ -112,8 +114,10 @@ Random verdict каждого GET по заданию независим; Redis
|
||||
| rate limit | window + 10–30% deterministic jitter |
|
||||
| realtime connection | 90s; set membership 120s |
|
||||
| coordination lock | 30s |
|
||||
| safety task | default 15m, обязательно > API poll max 300s + recovery margin |
|
||||
| safety cache | default 5–60m по rules version |
|
||||
| safety file hot cache | ≤30d; authoritative row/version в PostgreSQL |
|
||||
| safety text-rules cache | 48h; invalidation by rules version |
|
||||
| safety stable link policy cache | 48h |
|
||||
| safety DNS cache | actual DNS TTL, hard max 900s |
|
||||
|
||||
Новый key без TTL запрещён contract test, кроме Pub/Sub channel (не key) и ops metadata с явным обоснованием.
|
||||
|
||||
@@ -155,18 +159,18 @@ Eviction MVP: `volatile-lru`/`volatile-ttl`, так как все application ke
|
||||
DB0 rate = peak identities × routes × active windows × bytes/key
|
||||
DB0 idem = mutating requests/24h × avg sanitized record
|
||||
DB1 = peak connections × connection metadata + Pub/Sub buffers
|
||||
DB2 = safety tasks within TTL × avg task metadata
|
||||
total × 1.5 allocator/fragmentation × 1.3 growth reserve
|
||||
Redis Safety = hot verdict/link/DNS entries + rate windows + Pub/Sub buffers
|
||||
each instance total × 1.5 allocator/fragmentation × 1.3 growth reserve
|
||||
```
|
||||
|
||||
Pub/Sub output buffers и slow consumers имеют hard/soft limits. Load test фиксирует peak RPS, WS connections, idempotency response size и AOF rewrite headroom.
|
||||
|
||||
## 15. Auth, ACL и network boundary
|
||||
|
||||
Redis не публикует `6379` на host, подключён только к Docker `backend`. `protected-mode yes`, bind container interface, default user отключён. ACL users:
|
||||
Оба Redis не публикуют `6379` на host и подключены только к local Docker `backend` своей VM. `protected-mode yes`, default user отключён. ACL users:
|
||||
|
||||
- `api_backend`: DB0/DB1 key prefixes, нужные command categories;
|
||||
- `message_safety`: только DB2 prefixes;
|
||||
- `message_safety`: только Redis Safety prefixes;
|
||||
- `ops_health`: `PING`, ограниченный `INFO`;
|
||||
|
||||
Важно: Redis ACL не ограничивает logical DB напрямую надёжно; key-prefix patterns и разные credentials обязательны. `SELECT` запрещается, клиент URL сразу задаёт DB, но ACL prefix остаётся основной защитой.
|
||||
@@ -195,10 +199,10 @@ URL:
|
||||
```text
|
||||
REDIS_URL=redis://api_backend:<secret>@redis:6379/0
|
||||
REDIS_REALTIME_URL=redis://api_backend:<secret>@redis:6379/1
|
||||
MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/2
|
||||
MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/0
|
||||
```
|
||||
|
||||
Добавление credential env требует обновления arch-04 `.env.example`; до этого имена credential variables — TBD, URL может содержать injected secret.
|
||||
Первые два URL существуют только на ВМ1. На ВМ2 `MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/0`; credential доставляется secret file и не входит в общий `.env`.
|
||||
|
||||
## 17. Health и degraded behavior
|
||||
|
||||
@@ -211,7 +215,8 @@ MESSAGE_SAFETY_REDIS_URL=redis://message_safety:<secret>@redis:6379/2
|
||||
- profile/history GET могут работать под edge limits;
|
||||
- public GET использует bounded local conservative limiter/cache;
|
||||
- realtime cross-instance publish/coordination деградирует; REST/polling остаётся source of truth;
|
||||
- safety stub для digit task не может гарантировать GET task state — check возвращает `503`, а существующие task GET — `503`; синхронные text allow/deny могут работать только если policy явно разрешает Redis-independent path;
|
||||
- production Safety продолжает task claim/poll через PostgreSQL; hot cache/rate/wakeup деградируют и прогреваются после восстановления Redis;
|
||||
- legacy stub v1 может стать недоступным при потере своей DB2 до cutover;
|
||||
- internal inbox не теряется из-за Redis, так как durable receipt в PostgreSQL.
|
||||
|
||||
При latency выше threshold clients используют short timeout/circuit, не создают бесконечные retry storms. Reconnect — exponential backoff+jitter.
|
||||
@@ -224,7 +229,7 @@ Redis backup не используется для бизнес restore. Runbook:
|
||||
2. при целостном AOF/RDB восстановить на отдельном instance и проверить;
|
||||
3. иначе поднять пустой Redis;
|
||||
4. api-backend прогревает idempotency по durable records, realtime восстанавливается reconnect/polling;
|
||||
5. незавершённые safety tasks обрабатываются по service semantics/expire; api-backend durable `safety_tasks` сообщает dependency error/recovery.
|
||||
5. production safety tasks продолжают обрабатываться из PostgreSQL; Redis Safety прогревается лениво.
|
||||
|
||||
Не копировать Redis dump в небезопасное место: keys содержат UUID и hashed identifiers.
|
||||
|
||||
@@ -240,7 +245,7 @@ Redis backup не используется для бизнес restore. Runbook:
|
||||
- script errors/NOSCRIPT/slowlog;
|
||||
- rate limit decisions, idempotency hit/conflict/fallback;
|
||||
- Pub/Sub subscribers/output buffer/slow disconnect;
|
||||
- safety task create/get/expire.
|
||||
- Safety hot-cache hit/miss, DNS TTL cap и wakeup subscribers.
|
||||
|
||||
Alerts: unavailable, p99 latency, >80/90% memory, any sustained evictions, AOF error, no recent persistence, client buffer pressure, unexpected keys without TTL.
|
||||
|
||||
@@ -252,7 +257,7 @@ Alerts: unavailable, p99 latency, >80/90% memory, any sustained evictions, AOF e
|
||||
- idempotency same/different fingerprint, lock ownership, expiry, Redis loss + PostgreSQL fallback;
|
||||
- realtime heartbeat cleanup, duplicate disconnect, Pub/Sub loss + REST recovery;
|
||||
- locks expiry/late owner/fencing;
|
||||
- safety task TTL, concurrent polls и missing task;
|
||||
- Safety cache loss/rebuild, DNS TTL cap и доказательство отсутствия task/lease state в Redis;
|
||||
- `NOSCRIPT` reload;
|
||||
- all application keys имеют TTL;
|
||||
- max value/invalid serialization;
|
||||
@@ -279,6 +284,6 @@ Alerts: unavailable, p99 latency, >80/90% memory, any sustained evictions, AOF e
|
||||
|
||||
**Решения:** один instance/три DB MVP; AOF everysec + RDB; Pub/Sub best effort; PostgreSQL durable fallback; prefix ACL; все application keys с TTL.
|
||||
|
||||
**Допущения:** одна VM и одна replica API на старте; Redis loss допустим без потери business truth.
|
||||
**Допущения:** по одной Redis instance на ВМ1/ВМ2 и одна Safety API replica на старте; Redis loss допустим без потери business truth.
|
||||
|
||||
**TBD:** R1 точный maxmemory после load profile; R2 eviction policy после измерений; R3 credential env names в arch-04; R4 Safety task TTL/recovery margin; R5 TLS при изменении network topology; R6 момент разделения DB на instances; R7 RPO/RTO ops target.
|
||||
|
||||
+619
-142
File diff suppressed because it is too large
Load Diff
@@ -640,4 +640,4 @@ Business settings здесь не хранятся. Legacy `BITRIX_SYNC_FORWARD_
|
||||
- B-TBD4: retention/RPO/RTO и DLQ replay authorization.
|
||||
- B-TBD5: encryption key source/rotation runbook до production.
|
||||
- B-TBD6: точный CSP `frame-ancestors` placement.
|
||||
- B-TBD7: antivirus policy operator files остаётся у `api-backend`.
|
||||
- B7: operator files считаются trusted-channel данными MVP; api-backend применяет MIME/size/audit без Message Safety/AV, residual malware risk принят.
|
||||
|
||||
+711
-380
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# module-09. Наблюдаемость production-like контура
|
||||
|
||||
> Статус: целевая спецификация наблюдаемости MVP на одной VM.
|
||||
> Статус: целевая спецификация наблюдаемости MVP для ВМ1/ВМ2 и отдельного private SigNoz.
|
||||
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md)–[`module-08-keycloak.md`](module-08-keycloak.md).
|
||||
|
||||
## 1. Цели и границы
|
||||
@@ -20,11 +20,11 @@ Telemetry не является источником бизнес-истины
|
||||
|
||||
### 2.1. Обязательный минимум в основном Compose
|
||||
|
||||
Архитектура явно требует только `otel-collector`. Поэтому **основной production-like Compose обязан содержать Collector, но не обязан размещать Prometheus/Grafana/Loki/Tempo на той же VM**.
|
||||
Каждый root Compose (ВМ1 и ВМ2) обязан содержать локальный `otel-collector`. Prometheus/Grafana/Loki/Tempo не обязаны размещаться на application VM.
|
||||
|
||||
Предпочтительный operable-вариант после выбора backend:
|
||||
|
||||
1. приложения экспортируют OTLP gRPC в `otel-collector:4317`;
|
||||
1. приложения на каждой VM экспортируют OTLP gRPC только в свой local `otel-collector:4317`;
|
||||
2. Collector отправляет telemetry в выбранный удалённый управляемый OTLP backend провайдера;
|
||||
3. JSON stdout остаётся аварийным локальным журналом Docker с rotation;
|
||||
4. пока удалённый backend не выбран, допустим архитектурный минимум из arch-03: bounded JSON stdout/platform logs и Collector `debug` exporter с sampling в acceptance; такой режим не считается полноценным production-хранением и не закрывает alerting/SLO.
|
||||
@@ -309,6 +309,7 @@ CPU, load, memory/swap, disk usage/inodes/IO, network, container restarts/OOM, D
|
||||
### Message Safety
|
||||
|
||||
- checks/verdicts по `allow|deny|pending|error`;
|
||||
- `processing_mode`, active/used `config_version`, config activation result, `message_safety_mock_enabled` и forced outcomes по `text|file`/`allow|deny`;
|
||||
- poll duration/count buckets, timeout и recovery backlog age;
|
||||
- stub mode info и terminal `400` отдельно, пока действует test-only контракт;
|
||||
- cache hit, Redis latency, task expired/not-found.
|
||||
@@ -342,11 +343,11 @@ CPU, load, memory/swap, disk usage/inodes/IO, network, container restarts/OOM, D
|
||||
## 10. Dashboards
|
||||
|
||||
1. **Executive/SLO**: availability, error budget burn, p50/p95/p99, message delivery, auth, active incidents.
|
||||
2. **nginx edge**: RPS, 4xx/5xx, upstream latency/status, 429, WS, TLS, cache.
|
||||
2. **nginx ingress по VM**: отдельные панели/filters ВМ1 и ВМ2; RPS, 4xx/5xx, upstream latency/status, 429, WS, TLS, cache, CRM webhook на ВМ2.
|
||||
3. **api-backend**: routes, DB/Redis pools, JWKS, circuits, outbox/safety backlog, S3.
|
||||
4. **message-safety**: verdicts, pending/poll, task TTL, Redis, distribution stub outcomes.
|
||||
4. **message-safety**: capabilities, verdicts, `202` poll, PG queue age/leases/fencing, ClamAV/signature age, file/link cache и DNS dependency.
|
||||
5. **bitrix-local-app**: install/OAuth, connector, outbound/inbox/DLQ, API/Bitrix latency.
|
||||
6. **bitrix-sync**: mode, DB probe, last success/staleness; нельзя показывать CRM sync как рабочий в stub.
|
||||
6. **bitrix-sync**: mode/readiness, queue depth/oldest age, workflow/command transitions, CRM batch latency/subcommand outcome, limiter/throttle, retry/DLQ, webhook/reconciliation lag, mapping invariants и business-alert SLA.
|
||||
7. **Keycloak**: login/OTP/lockout, sessions/tokens, provider settings cache, JVM/DB.
|
||||
8. **Redis**: memory/evictions/AOF/latency/clients/keyspace.
|
||||
9. **PostgreSQL/S3**: provider metrics, storage, connections, backup/PITR, object errors.
|
||||
@@ -386,12 +387,18 @@ CPU, load, memory/swap, disk usage/inodes/IO, network, container restarts/OOM, D
|
||||
- TLS expiry <14 дней warning, <7 дней page;
|
||||
- disk >85% warning, >92% page; OOM/restart loop;
|
||||
- managed PG backup/PITR failure.
|
||||
- Message Safety active config missing/invalid, referenced artifact unavailable или config refresh stale >5 с.
|
||||
- `message_safety_mock_enabled=1` — active page/high-severity alert без auto-resolve по времени; закрывается только после возврата в standard.
|
||||
- bitrix-sync invalid/revoked credential или обязательная configuration/grant missing;
|
||||
- bitrix-sync technical DLQ >0, mapping invariant violation или worker/limiter heartbeat stale;
|
||||
- bitrix-sync queue oldest age >30 с при healthy CRM либо sustained рост;
|
||||
- Contact webhook/reconciliation cursor lag выше двух configured intervals.
|
||||
|
||||
### Ticket/warning alerts
|
||||
|
||||
- p95 regression 20% release-over-release;
|
||||
- settings/JWKS cache stale;
|
||||
- bitrix-sync stub probe stale >150s;
|
||||
- bitrix-sync CRM last success stale, rate-limit errors выше baseline или business alert SLA overdue;
|
||||
- OAuth expires <24h без успешного refresh;
|
||||
- quarantine orphan growth;
|
||||
- cardinality/ingest growth >2× baseline.
|
||||
@@ -453,6 +460,8 @@ Legal retention/erasure имеет приоритет; изменение тре
|
||||
- restart policy с backoff;
|
||||
- приложения имеют bounded non-blocking OTLP exporter queue.
|
||||
|
||||
Collector существует отдельным экземпляром на ВМ1 и ВМ2. Каждый имеет собственный `otel-queue` volume/limits и экспортирует в private SigNoz `192.168.0.5:4317`; ВМ2 никогда не использует Docker hostname collector ВМ1. Telemetry outage/overflow fail-open для business и Safety readiness, но создаёт alert.
|
||||
|
||||
Доступ к Docker socket запрещён. Для container metrics используется безопасный exporter/hostmetrics, а не unrestricted socket mount.
|
||||
|
||||
## 15. Security
|
||||
@@ -521,7 +530,7 @@ Legal retention/erasure имеет приоритет; изменение тре
|
||||
|
||||
## 17. Проверки и Definition of Done
|
||||
|
||||
- Collector config проходит validate и запускается в едином Compose;
|
||||
- Collector config проходит validate и запускается в root Compose каждой VM;
|
||||
- OTLP gRPC и HTTP принимают три сигнала;
|
||||
- все сервисы имеют правильные resource attributes;
|
||||
- request проходит nginx/API/Safety/Open Lines с одним `request_id` и связанным trace;
|
||||
@@ -541,7 +550,7 @@ Legal retention/erasure имеет приоритет; изменение тре
|
||||
|
||||
### Решения
|
||||
|
||||
- O1: обязательный архитектурный минимум — Collector; выбран self-hosted SigNoz
|
||||
- O1: обязательный архитектурный минимум — local Collector на каждой application VM; выбран self-hosted SigNoz
|
||||
на отдельной VM `192.168.0.5`, доступный по приватному OTLP gRPC.
|
||||
- O2: Prometheus/Grafana/Loki/Tempo — отдельный operable profile, не скрытая обязательная нагрузка основной VM.
|
||||
- O3: JSON stdout — аварийный локальный buffer; audit App DB — durable.
|
||||
@@ -562,7 +571,7 @@ Legal retention/erasure имеет приоритет; изменение тре
|
||||
|
||||
1. `arch-03` разрешает stdout/platform exporter как минимум, но production-like расследования и alerts без backend ограничены. Здесь remote OTLP backend рекомендован, но не объявлен выбранным: провайдер остаётся TBD.
|
||||
2. `module-05` использует test-only terminal `400` и non-sticky verdict вместо canonical `403`/sticky production verdict. Dashboards обязаны маркировать сервис `stub`; production SLO Safety на нём недостоверен.
|
||||
3. `module-07` — только DB connectivity stub, тогда как arch-01/02 описывают полноценную CRM sync. Dashboard не должен показывать queue/CRM SLI, которых нет.
|
||||
3. `module-07` задаёт full sync target; до code/portal cutover dashboard обязан показывать `sync_disabled`, а не синтетический CRM success. Queue/CRM SLI включаются только после module-07 preflight.
|
||||
4. Retention, RPO/RTO и production SLO открыты в module-01/04/06/08; значения этого документа являются initial ops policy, не закрывают legal/product TBD.
|
||||
5. Observability env (`OTEL_REMOTE_*`, service names, sampling/queue limits)
|
||||
добавлены в arch-04 и `.env.example`; sampling/queue limits уточняются после
|
||||
|
||||
@@ -1,34 +1,47 @@
|
||||
# module-10. Runbook развёртывания HAN Chat
|
||||
|
||||
> Статус: последовательная инструкция первого production-like деплоя и эксплуатации на одной Ubuntu VM.
|
||||
> Статус: целевой runbook ВМ1/ВМ2. Команды существующего stub-контура применимы только до production Safety cutover и явно отмечены как legacy.
|
||||
> Все значения в `<УГЛОВЫХ_СКОБКАХ>` — placeholders. Команды с `cd <BACKEND_ROOT>` требуют подстановки реального пути корня backend-репозитория на VM.
|
||||
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](arch-00-glossary.md), [`arch-01-system-architecture.md`](arch-01-system-architecture.md), [`arch-02-api-contracts.md`](arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](arch-05-agent-development-process.md), [`module-01-api-backend.md`](module-01-api-backend.md)–[`module-09-observability.md`](module-09-observability.md).
|
||||
> Канонические источники: [`../architectory/README.md`](../architectory/README.md), [`../architectory/arch-00-glossary.md`](../architectory/arch-00-glossary.md), [`../architectory/arch-01-system-architecture.md`](../architectory/arch-01-system-architecture.md), [`../architectory/arch-02-api-contracts.md`](../architectory/arch-02-api-contracts.md), [`../architectory/arch-03-docker-compose-blueprint.md`](../architectory/arch-03-docker-compose-blueprint.md), [`../architectory/arch-04-settings-and-content.md`](../architectory/arch-04-settings-and-content.md), [`../architectory/arch-05-agent-development-process.md`](../architectory/arch-05-agent-development-process.md), [`../architectory/arch-06-service-hosting-security.md`](../architectory/arch-06-service-hosting-security.md), [`module-01-api-backend.md`](module-01-api-backend.md)–[`module-09-observability.md`](module-09-observability.md).
|
||||
|
||||
## 1. Неподвижные правила
|
||||
|
||||
1. Один root `docker compose` запускается из `<BACKEND_ROOT>`.
|
||||
2. Ровно один edge nginx публикует `80/443`.
|
||||
3. API, Keycloak, Redis, OTEL, Safety и Bitrix-сервисы не имеют host `ports`.
|
||||
1. Один root Compose project описывается в `<BACKEND_ROOT>`; в steady state его запускает root-owned systemd-unit/helper, а не пользователь из группы `docker`.
|
||||
2. На каждой VM ровно один nginx; ВМ1 и ВМ2 имеют независимые public DNS/TLS ingress и deployment lifecycle.
|
||||
3. Application containers не имеют public host ports. Nginx ВМ1 публикует свой `80/443`; nginx ВМ2 — отдельный `80/443` только для exact CRM webhook и private `8443` для Message Safety/internal access.
|
||||
4. `/internal/*` не маршрутизируется публично.
|
||||
5. Managed PostgreSQL находится вне Compose, в той же VPC, без public IP.
|
||||
6. S3 — внешний Selectel-compatible storage; клиент получает только presigned URL.
|
||||
7. Секреты не коммитятся, не вставляются в команды shell history и не выводятся в отчёты.
|
||||
8. Миграции выполняются отдельными one-shot steps до новой версии приложения.
|
||||
9. Message Safety запускается как documented stub до замены; это не production antivirus/moderation.
|
||||
10. `bitrix-sync` запускается как DB-connectivity stub; полноценной CRM sync нет.
|
||||
10. `bitrix-sync` вводится только после выполнения preflight/cutover gates module-07; до этого `BITRIX_SYNC_ENABLED=false`, public webhook закрыт на edge.
|
||||
11. На ВМ1 и ВМ2 отдельные root Compose projects/systemd units; deploy/rollback выполняются независимо.
|
||||
11. ВМ2 — самостоятельная service VM с минимальным public webhook ingress, allow-listed egress, отдельным IAM principal и service-specific secret files.
|
||||
12. OS-роли, SSH/sudo, secrets delivery, container hardening и private-VM lockdown подчиняются arch-06.
|
||||
|
||||
Прямые `docker compose` команды в этом runbook выполняются `admin` только при bootstrap/recovery либо инкапсулируются в утверждённые root-owned systemd-units. Они не являются основанием выдавать `deploy` доступ к Docker daemon.
|
||||
|
||||
## 2. Роли и обозначения
|
||||
|
||||
- **Cloud admin**: VPC, VM, PG, S3, DNS/security groups.
|
||||
- **Deploy operator**: VM, Compose, migrations, release/rollback.
|
||||
- **Deploy operator / OS user `deploy`**: запуск утверждённых release/rollback/migration systemd-units и root-owned Message Safety mode helper; без группы `docker`, записи в production compose/unit/scripts/config и общего sudo.
|
||||
- **Break-glass OS user `admin`**: bootstrap и аварийное восстановление; не используется для штатного деплоя.
|
||||
- **OS user `tunnel`**: только allow-listed local TCP forwarding к private endpoints; без sudo/shell operations.
|
||||
- **Bitrix admin**: local app, connector, Open Line 8, callbacks.
|
||||
- **Security owner**: secrets, Keycloak admin MFA, firewall, retention.
|
||||
- **Safety Service Owner**: API/data contract, capacity result и v2 cutover/rollback sign-off.
|
||||
- **Rule Pack Owner**: rules bundle, corpus, monitor report и version release.
|
||||
- **Product Owner**: mnemonic `safety.chat.blocked` и business acceptance chat flow.
|
||||
- **Operations Owner**: VM2 alerts, ClamAV signatures, incident/reprovision/restore rehearsal.
|
||||
|
||||
Placeholders:
|
||||
|
||||
```text
|
||||
<PUBLIC_HOST> например chat.example.ru
|
||||
<VM_PUBLIC_IP> публичный IPv4 VM
|
||||
<PROCESSING_PUBLIC_HOST> отдельный public host ВМ2, например processing.example.ru
|
||||
<VM2_PUBLIC_IP> публичный IPv4/LB address ВМ2
|
||||
<VM_PRIVATE_IP> приватный IPv4 VM
|
||||
<VPC_CIDR> например 10.20.0.0/24
|
||||
<PG_PRIVATE_HOST> private FQDN/IP managed PG
|
||||
@@ -58,8 +71,9 @@ Placeholders:
|
||||
- remote observability backend;
|
||||
- RPO/RTO и maintenance window;
|
||||
- ответственных за alerts/Bitrix/Keycloak.
|
||||
- назначенные Safety Service/Rule Pack/Product/Security/Operations owners и approvals cutover.
|
||||
|
||||
Начальный sizing без local Grafana stack:
|
||||
Начальный sizing ВМ2 без local Grafana stack:
|
||||
|
||||
- VM: 4 vCPU, 8 ГБ RAM, 80 ГБ SSD, 4 ГБ swap;
|
||||
- managed PG: минимум 2 vCPU, 4 ГБ RAM, 50 ГБ, HA по возможности;
|
||||
@@ -67,7 +81,16 @@ Placeholders:
|
||||
- OTEL Collector: 512 МиБ + 5–10 ГБ queue;
|
||||
- свободный диск VM после pull/build: не менее 30%.
|
||||
|
||||
Это baseline, не гарантия. До real traffic обязателен load test с long Safety poll и WS.
|
||||
Это baseline, не гарантия. До real traffic обязателен load test module-05 §15.4:
|
||||
|
||||
- sustained 10 text checks/s: p95 ≤2 с, p99 ≤5 с;
|
||||
- sustained 2 file checks/s на 5 worker slots: среднее processing ≤2.5 с, p95 ≤60 с, public wait ≤300 с;
|
||||
- 100 pending принимаются; 101-й file POST получает retryable `503` без новой task;
|
||||
- RPS overflow даёт `429 + Retry-After`;
|
||||
- long Safety poll не блокирует WS/read API;
|
||||
- если gate не пройден, увеличить slots/CPU/clamd scan lanes и повторить; production traffic не открывать.
|
||||
|
||||
Monthly availability SLO для MVP не задаётся; это не отменяет latency/load gates и alerts.
|
||||
|
||||
### Gate 0
|
||||
|
||||
@@ -83,30 +106,37 @@ Placeholders:
|
||||
|
||||
### 4.1. Сеть
|
||||
|
||||
Создать одну private subnet для VM и managed PG. PG получает только private address. VM имеет public IP только для nginx/SSH.
|
||||
Создать private subnet для ВМ1, ВМ2, SigNoz и managed PG. ВМ1 и ВМ2 имеют отдельные public IP/LB только для своих nginx; service-to-service и PostgreSQL traffic остаётся private. SSH к обеим VM — только ops VPN/bastion.
|
||||
|
||||
Security groups:
|
||||
|
||||
| Source | Destination | Port | Rule |
|
||||
|---|---|---:|---|
|
||||
| trusted ops CIDR/VPN | VM | SSH `<SSH_PORT>` | allow |
|
||||
| internet | VM | TCP 80 | allow для redirect/ACME |
|
||||
| internet | VM | TCP 443 | allow |
|
||||
| VM private IP/SG | managed PG | `<PG_PORT>` | allow |
|
||||
| VM | internet | 443 | allow egress: registry, Bitrix, S3, OTLP, ACME, i-Digital Direct |
|
||||
| trusted ops CIDR/VPN | ВМ1, ВМ2 | SSH `<SSH_PORT>` | allow |
|
||||
| internet | ВМ1 | TCP 80/443 | allow edge redirect/ACME/application |
|
||||
| internet | nginx ВМ2 | TCP 80/443 | allow ACME/redirect + exact CRM webhook |
|
||||
| ВМ1 SG | ВМ2 | TCP 8443 | private TLS only |
|
||||
| ВМ1/ВМ2 service SG | managed PG | `<PG_PORT>` | allow по нужным DB roles |
|
||||
| ВМ2 collector | private SigNoz | TCP 4317 | allow |
|
||||
| ВМ2 workers | S3 endpoints | TCP 443 | allow |
|
||||
| ВМ2 `bitrix-sync` | approved Bitrix portal | TCP 443 | allow |
|
||||
| ВМ2 `freshclam` | approved signature CDN | TCP 443/80 по vendor manifest | allow |
|
||||
| ВМ2 | trusted DNS/NTP | UDP/TCP 53, UDP 123 | allow |
|
||||
| internet | managed PG | any | deny |
|
||||
| internet | VM | 6379, 4317, 4318, 8000, 8080, 9000 | deny |
|
||||
| internet | ВМ2 | any кроме nginx 80/443 | deny ingress |
|
||||
| internet | обе VM | 6379, 4317, 4318, 8000, 8080, 8443, 9000 | deny public |
|
||||
|
||||
Если cloud SG не поддерживает egress allow-list, оставить egress open и контролировать destinations приложением/TLS; не ломать S3/Bitrix/OIDC.
|
||||
ВМ2 использует default-deny egress. Registry/OS repositories открываются только в bootstrap/controlled window и затем снова закрываются. Если provider SG не умеет destination allow-list, применяется host firewall/proxy/NAT policy; постоянный open egress для ВМ2 не является допустимым production состоянием.
|
||||
|
||||
### 4.2. DNS
|
||||
|
||||
Создать `A <PUBLIC_HOST> → <VM_PUBLIC_IP>`. Не добавлять `www`, если он не нужен и не включён в certificate. Для отдельного API host действуют правила arch-03; MVP предпочтительно использует один host с paths.
|
||||
Создать `A <PUBLIC_HOST> → <VM1_PUBLIC_IP>`, `A <PROCESSING_PUBLIC_HOST> → <VM2_PUBLIC_IP>` и private DNS `processing.internal → <VM2_PRIVATE_IP>`. Public host ВМ2 используется только CRM webhook; private name не публикуется во внешнем DNS.
|
||||
|
||||
Проверка с рабочей станции:
|
||||
|
||||
```bash
|
||||
dig +short <PUBLIC_HOST>
|
||||
dig +short <PROCESSING_PUBLIC_HOST>
|
||||
```
|
||||
|
||||
Ответ должен совпасть с `<VM_PUBLIC_IP>`.
|
||||
@@ -143,8 +173,9 @@ sudo DEPLOY_USER=deploy \
|
||||
- проверить его версию/review;
|
||||
- не передавать реальные IP/ключи в git;
|
||||
- проверить auto reboot unattended upgrades относительно maintenance;
|
||||
- решить, действительно ли deploy user нужен в группе `docker` (это root-equivalent);
|
||||
- не применять `AllowTcpForwarding no`, если утверждённый break-glass DB tunnel необходим; предпочтителен VPN/bastion.
|
||||
- гарантировать, что `deploy` не добавлен в группу `docker` (это root-equivalent);
|
||||
- оставить `AllowTcpForwarding no` по умолчанию; если нужен DB tunnel, создать отдельного `tunnel` с `AllowTcpForwarding local`, конкретным `PermitOpen`, без sudo/TTY/agent/X11 forwarding;
|
||||
- установить root-owned systemd-units и `/etc/sudoers.d/deploy` с полными командами и конкретными unit names без wildcard.
|
||||
|
||||
### 5.2. Проверки
|
||||
|
||||
@@ -160,12 +191,14 @@ df -h
|
||||
free -h
|
||||
```
|
||||
|
||||
Открыть второй SSH session как `deploy`, затем отключить root/password login. `.env` позже имеет mode `0600`.
|
||||
Открыть второй SSH session как `deploy`, затем отключить root/password login. Production `.env` позже содержит только non-secret config; runtime secrets доставляются по arch-06.
|
||||
|
||||
### Gate 2
|
||||
|
||||
- [ ] SSH key login `deploy` проверен во втором сеансе.
|
||||
- [ ] Root/password auth выключены.
|
||||
- [ ] `deploy` не состоит в группе `docker`; `sudo -l` содержит только утверждённые конкретные systemd-команды.
|
||||
- [ ] Production compose, units, deploy scripts и secret mappings принадлежат root и недоступны `deploy` на запись.
|
||||
- [ ] UFW и DOCKER-USER активны после restart Docker.
|
||||
- [ ] Docker Engine/Compose plugin закреплены поддерживаемой версией.
|
||||
- [ ] NTP active; disk/swap соответствуют sizing.
|
||||
@@ -173,6 +206,32 @@ free -h
|
||||
|
||||
**Ожидаемый результат:** reboot VM не теряет SSH, firewall и Docker service.
|
||||
|
||||
### 5.3. Дополнительный lockdown private/no-egress VM
|
||||
|
||||
Для SigNoz и другой VM, которая после раскатки не должна иметь internet ingress/egress, bootstrap выполняется по lifecycle arch-06.
|
||||
|
||||
До закрытия временного доступа:
|
||||
|
||||
- [ ] SSH разрешён только из trusted ops CIDR и только по ключам.
|
||||
- [ ] Пакеты/images получены из утверждённых источников, версии/digests зафиксированы.
|
||||
- [ ] Health/readiness успешны.
|
||||
- [ ] Проверены необходимые private flows (например app-VM → OTLP/SigNoz).
|
||||
- [ ] Проверен private путь администрирования через bastion/VPN.
|
||||
- [ ] Секреты размещены root-owned файлами; временные копии удалены.
|
||||
|
||||
Lockdown:
|
||||
|
||||
- [ ] Public IP удалён, если поддерживается и не нужен.
|
||||
- [ ] Public SSH и любой internet ingress удалены из cloud SG и host firewall.
|
||||
- [ ] Общий internet egress закрыт; оставлены только явно утверждённые private flows.
|
||||
- [ ] С внешней машины SSH и service ports недоступны.
|
||||
- [ ] С VM не проходит неразрешённый internet egress.
|
||||
- [ ] Из private network работают SSH и обязательные service flows.
|
||||
- [ ] Временные bootstrap credentials/rules/files удалены.
|
||||
- [ ] Результат и время закрытия записаны в release checklist.
|
||||
|
||||
Повторное открытие ingress/egress выполняется только как ограниченная по CIDR/destination и времени break-glass операция. После неё весь lockdown checklist повторяется.
|
||||
|
||||
## 6. Stage 3 — managed PostgreSQL
|
||||
|
||||
### 6.1. Защита и восстановление managed PostgreSQL
|
||||
@@ -188,7 +247,7 @@ free -h
|
||||
|
||||
CA managed PostgreSQL скачать из панели или документации провайдера в защищённый путь VM, например `/opt/han-chat/secrets/pg/ca.pem`, с владельцем `root:deploy` и mode `0440` (либо `0400`, если файл читает один пользователь). Все DSN используют `sslmode=verify-full` и `sslrootcert=/run/secrets/pg-ca.pem` либо эквивалент драйвера. Режимы `disable`, `allow`, `prefer` и `require` без проверки CA для production запрещены.
|
||||
|
||||
Публичный HTTPS-сертификат `<PUBLIC_HOST>` выпускается отдельно через **Let's Encrypt** на Stage 9. Он устанавливается в корневой nginx, а не в PostgreSQL.
|
||||
Публичные HTTPS-сертификаты `<PUBLIC_HOST>` и `<PROCESSING_PUBLIC_HOST>` выпускаются отдельно через **Let's Encrypt** на Stage 9 и устанавливаются только в nginx соответствующей VM, а не в PostgreSQL.
|
||||
|
||||
### 6.2. Роли
|
||||
|
||||
@@ -207,7 +266,7 @@ CA managed PostgreSQL скачать из панели или документа
|
||||
5. runtime: `USAGE`, DML и sequence grants только на свои objects;
|
||||
6. `ALTER DEFAULT PRIVILEGES` от migration owner;
|
||||
7. запретить чужие schemas и public schema create;
|
||||
8. `bitrix_sync_user` не получает `han_app` grants, пока module-07 остаётся stub.
|
||||
8. `bitrix_sync_user` получает только column/table grants и approved procedures из module-07 §13 после применения полной sync migration; broad schema write запрещён.
|
||||
|
||||
Команда bootstrap требует `<PROJECT_PATH>`:
|
||||
|
||||
@@ -241,7 +300,7 @@ psql "host=<PG_PRIVATE_HOST> port=<PG_PORT> dbname=<PG_DATABASE> user=<RUNTIME_U
|
||||
1. `api-backend` Alembic владеет `han_app`, triggers, seed;
|
||||
2. `bitrix-local-app` Alembic владеет `bitrix_local`;
|
||||
3. `message-safety` stub не создаёт PG tables до production implementation;
|
||||
4. `bitrix-sync` stub — optional empty baseline;
|
||||
4. `bitrix-sync` migration role владеет schema `bitrix_sync`; api-backend Alembic отдельно мигрирует shared `han_app.sync_queue`, triggers и mapping;
|
||||
5. Keycloak мигрирует standard tables сам; custom provider имеет собственные versioned migrations;
|
||||
6. `sms-service` владеет versioned migrations/seed schema `sms`; runtime `sms_user` не имеет доступа к `han_app`/`keycloak`.
|
||||
|
||||
@@ -282,18 +341,18 @@ CORS quarantine:
|
||||
{
|
||||
"AllowedOrigins": ["https://<PUBLIC_HOST>"],
|
||||
"AllowedMethods": ["PUT"],
|
||||
"AllowedHeaders": ["Content-Type", "x-amz-*"],
|
||||
"AllowedHeaders": ["Content-Type", "If-None-Match", "x-amz-checksum-sha256", "x-amz-*"],
|
||||
"ExposeHeaders": ["ETag", "x-amz-checksum-sha256"],
|
||||
"MaxAgeSeconds": 600
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Уточнить фактические required signed headers. Не разрешать `*` origin с credentials.
|
||||
Required signed headers для target flow: `Content-Type`, `If-None-Match: *`, checksum. Не разрешать `*` origin с credentials.
|
||||
|
||||
Lifecycle:
|
||||
|
||||
- quarantine: expire orphan objects только после периода, превышающего Safety poll + recovery; initial 2 дня, согласовать;
|
||||
- quarantine: failed/orphan expire через 48 ч и только при отсутствии active `safety_tasks`;
|
||||
- incomplete multipart upload: abort через 1 день;
|
||||
- attachments/documents: без auto-delete до legal retention;
|
||||
- noncurrent versions: policy после legal review.
|
||||
@@ -303,7 +362,10 @@ Lifecycle:
|
||||
- [ ] Все buckets private.
|
||||
- [ ] API key не может list/write вне exact scope.
|
||||
- [ ] Safety key не может write/delete.
|
||||
- [ ] Browser test origin выполняет presigned PUT.
|
||||
- [ ] Browser test origin выполняет presigned PUT с checksum и `If-None-Match: *`; повтор того же key получает `412`.
|
||||
- [ ] Complete фиксирует authoritative `version_id`, ETag и checksum; Safety читает только эту version.
|
||||
- [ ] Wrong version/ETag и изменённый source дают deny/error и не promote-ятся.
|
||||
- [ ] Conditional promote mismatch не создаёт delivery outbox/Bitrix call.
|
||||
- [ ] Quarantine lifecycle не удалит active `safety_tasks`.
|
||||
- [ ] Data lifecycle соответствует retention.
|
||||
|
||||
@@ -483,7 +545,11 @@ cd <BACKEND_ROOT>
|
||||
docker compose config --services
|
||||
```
|
||||
|
||||
В целевом real-SMS release ожидаются: `nginx`, `api-backend`, `message-safety`, `keycloak`, `sms-service`, `sms-worker` (либо документированный worker process), `bitrix-sync`, `bitrix-local-app`, `redis`, `otel-collector` и one-shot jobs/profile components.
|
||||
В target release:
|
||||
|
||||
- root Compose ВМ1: edge `nginx`, `api-backend`, `keycloak`, `sms-service`, `sms-worker`, `bitrix-local-app`, Redis DB0/DB1, local `otel-collector`;
|
||||
- root Compose ВМ2: собственный public/private nginx, `message-safety-api`, `message-safety-worker`, `clamd`, `freshclam`, `bitrix-sync`, Redis Safety, local `otel-collector`;
|
||||
- API и worker Safety используют один immutable image, но отдельные processes/containers. MVP: 1 API + 1 worker container с 5 file-worker slots; при провале gates сначала увеличиваются slots/worker replicas по queue depth.
|
||||
|
||||
Networks:
|
||||
|
||||
@@ -520,7 +586,7 @@ Redis: ACL, AOF everysec, RDB, maxmemory, volume, no host port. OTEL: config rea
|
||||
|
||||
## 12. Stage 9 — TLS bootstrap, фаза 1
|
||||
|
||||
Прототип `ssl-issue.sh` останавливает весь Compose и использует standalone Certbot. Для full stack предпочтителен **webroot two-phase**, чтобы не делать `compose down`.
|
||||
Прототип `ssl-issue.sh` останавливает весь Compose и использует standalone Certbot. Для full stack предпочтителен **webroot two-phase**, чтобы не делать `compose down`. Процедура выполняется независимо в root Compose каждой VM: для ВМ1 с `<PUBLIC_HOST>`, для ВМ2 с `<PROCESSING_PUBLIC_HOST>`.
|
||||
|
||||
### Phase A: HTTP bootstrap
|
||||
|
||||
@@ -528,7 +594,7 @@ Redis: ACL, AOF everysec, RDB, maxmemory, volume, no host port. OTEL: config rea
|
||||
2. Запустить nginx с bootstrap config: только `/.well-known/acme-challenge/` и redirect; TLS block не требует отсутствующий cert.
|
||||
3. Запустить Certbot profile:
|
||||
|
||||
Сначала проверить процесс через Let's Encrypt staging CA, добавив `--staging`. Staging-сертификат не является доверенным браузерами и нужен только для проверки DNS, firewall, ACME webroot и конфигурации. После успешной проверки удалить staging lineage либо выпустить production-сертификат с отдельным `--cert-name <PUBLIC_HOST>`.
|
||||
Сначала проверить процесс через Let's Encrypt staging CA, добавив `--staging`. Staging-сертификат не является доверенным браузерами и нужен только для проверки DNS, firewall, ACME webroot и конфигурации. После успешной проверки удалить staging lineage либо выпустить production-сертификат с отдельным `--cert-name` текущего host. Ни сертификат, ни ACME volume между VM не разделяются.
|
||||
|
||||
Production-выпуск:
|
||||
|
||||
@@ -654,7 +720,7 @@ docker compose run --rm api-backend alembic upgrade head
|
||||
docker compose run --rm bitrix-local-app alembic upgrade head
|
||||
```
|
||||
|
||||
Для message-safety stub PG migration отсутствует. Для bitrix-sync stub — baseline только если реализован. Keycloak стандартную schema мигрирует выбранная pinned версия при controlled startup; provider migration выполняется отдельным approved job.
|
||||
Для message-safety stub PG migration отсутствует. Перед full sync cutover сначала применяется backward-compatible api-backend migration shared queue/mapping/trigger, затем migration schema `bitrix_sync`; grants выдаются после обеих migrations и negative permission test. Keycloak стандартную schema мигрирует выбранная pinned версия при controlled startup; provider migration выполняется отдельным approved job.
|
||||
|
||||
### 13.3. Seed `app_settings`
|
||||
|
||||
@@ -698,6 +764,35 @@ Production cutover запрещён при любом placeholder, несогл
|
||||
|
||||
Rollback SMS: немедленно вернуть Keycloak в mock mode; не удалять schema/journal и не откатывать migrations без доказанной backward compatibility. Остановить новые real orders, дать worker завершить либо зафиксировать in-flight/`uncertain`; предпочтителен forward-fix.
|
||||
|
||||
### 13.5. Controlled rollout `bitrix-sync`
|
||||
|
||||
До включения `BITRIX_SYNC_ENABLED=true`:
|
||||
|
||||
1. создать/проверить custom Contact fields `user_id`, registration flag, citizenship и занести их non-secret names в env; для universal CRM имя `UF_CRM_<digits>` преобразуется в `ufCrm_<digits>`, active filter использует `1`, а wire-значения add/update подтверждаются contract test;
|
||||
2. создать smart process «Конфликты синхронизации», поля/стадии/ответственного/SLA и активировать validated `bitrix_sync.settings`;
|
||||
3. завести отдельный входящий webhook технического пользователя с минимальными правами module-07 §13;
|
||||
4. настроить два HTTP-webhook робота на Contact/alert receiver URLs `https://<PROCESSING_PUBLIC_HOST>/bitrix/sync/webhook/...?token=<receiver-token>`; использовать отдельные высокоэнтропийные query tokens из штатного secret manager, исключить query/body из журналов; local app/event handler для CRM sync не создавать;
|
||||
5. применить expand migrations `han_app`, затем `bitrix_sync`, после чего выдать точечные GRANT и выполнить negative permission tests;
|
||||
6. зафиксировать `cutover_watermark` и one-shot операцией отменить существующие до него pending/retry contact-задачи с причиной `initial_full_sync_cutover`;
|
||||
7. не создавать backfill: это утверждённое ограничение первого релиза;
|
||||
8. запустить image с sync disabled, проверить `/health/live`, expected `sync_disabled`, secret/config validation, portal host/member ID и smoke разрешённых Bitrix methods, включая `crm.item.list` для `entityTypeId=3` с `>=updatedTime`, `opened=1` и registration field `=1`;
|
||||
9. открыть на nginx ВМ2 только два exact webhook routes для version-controlled `BITRIX_WEBHOOK_ALLOWED_CIDRS`, выполнить nginx config test, valid/invalid source IP и query-token form-urlencoded contract tests, подтвердить отсутствие query/body в logs/traces и отсутствие запросов на ВМ1;
|
||||
10. включить sync, проверить обработку только post-watermark canary user, mapping в `bitrix_sync`, отсутствие CRM ID в App DB, suppression, alert/rebind и telemetry;
|
||||
11. наблюдать не менее agreed canary window queue age, CRM limit errors, DLQ, webhook/reconciliation lag, число Contact, восстановленных reconciliation без webhook, source-IP rejects и SigNoz alerts; scheduled full reconciliation не запускать.
|
||||
|
||||
Rollback: закрыть public webhook routes на nginx ВМ2 либо вернуть retryable `503`, остановить claims, bounded drain in-flight, установить `BITRIX_SYNC_ENABLED=false`. ВМ1 не изменяется. Уже созданные Contact/mapping автоматически не удалять; pending после watermark не отменять; schema downgrade с production rows не выполнять.
|
||||
|
||||
#### Изменение source IP Битрикс24
|
||||
|
||||
Сигнал для проверки allow-list — всплеск Contact, восстановленных инкрементальной reconciliation без обработанного webhook, особенно одновременно с ростом `webhook_rejected_total{reason="source_ip"}`.
|
||||
|
||||
1. Сопоставить окно всплеска с bounded-retention журналом source-IP rejects nginx ВМ2; query и body не извлекать и не сохранять.
|
||||
2. Подтвердить принадлежность нового адреса инфраструктуре Битрикс24/портала по согласованному каналу или контролируемым probe. Наличие корректного query token само по себе не является подтверждением.
|
||||
3. Добавить минимально необходимый IP/CIDR в version-controlled `BITRIX_WEBHOOK_ALLOWED_CIDRS`, выполнить peer review, manifest validation и `nginx -t` через штатный deployment unit.
|
||||
4. Применить safe reload, проверить приём Contact/alert webhook и отсутствие query/body в logs/traces.
|
||||
5. Убедиться, что source-IP rejects прекратились, webhook lag нормализовался, а следующие инкрементальные reconciliation run не показывают растущих восстановлений.
|
||||
6. При ошибочном расширении немедленно вернуть предыдущую approved версию allow-list. Автоматическое добавление наблюдаемого IP запрещено.
|
||||
|
||||
## 14. Stage 11 — Keycloak bootstrap
|
||||
|
||||
### 14.1. Первый старт
|
||||
@@ -749,33 +844,13 @@ Custom OTP tables мигрируются versioned mechanism до включен
|
||||
|
||||
Архитектурный порядок:
|
||||
|
||||
1. Redis;
|
||||
2. OTEL Collector;
|
||||
3. API backend/settings;
|
||||
4. SMS service/worker после migrations (при SMS release; Keycloak пока mock);
|
||||
5. Keycloak;
|
||||
6. Message Safety;
|
||||
7. Bitrix local app;
|
||||
8. Bitrix sync;
|
||||
9. nginx.
|
||||
1. На ВМ2 approved systemd unit поднимает Redis Safety и local Collector.
|
||||
2. Затем `clamd`/`freshclam`, Safety API/worker и `bitrix-sync`.
|
||||
3. Последним на ВМ2 поднимается nginx с независимыми public `80/443` и private `8443` server blocks; проверяются оба TLS-контура, exact webhook routes, capability health, signature age и отрицательные ingress/egress tests.
|
||||
4. На ВМ1 unit поднимает локальные Redis/Collector, API, SMS, Keycloak, local app и edge nginx.
|
||||
5. Только private `MESSAGE_SAFETY_URL` ВМ1 переключается на ВМ2 после Safety gates. Public CRM webhook DNS/routes ВМ2 разворачиваются независимо и не требуют изменения ВМ1.
|
||||
|
||||
Команды:
|
||||
|
||||
```bash
|
||||
cd <BACKEND_ROOT>
|
||||
docker compose up -d redis
|
||||
docker compose up -d otel-collector
|
||||
docker compose up -d api-backend
|
||||
docker compose up -d sms-service sms-worker
|
||||
docker compose up -d keycloak
|
||||
docker compose up -d message-safety
|
||||
docker compose up -d bitrix-local-app bitrix-sync
|
||||
docker compose up -d nginx
|
||||
docker compose up -d --wait api-backend keycloak sms-service bitrix-local-app
|
||||
docker compose exec -T nginx nginx -t -c /tmp/nginx.conf
|
||||
docker compose kill -s HUP nginx
|
||||
docker compose ps
|
||||
```
|
||||
Legacy single-VM `docker compose up` из старого stub-контура не является evidence готовности target ВМ2.
|
||||
|
||||
При повторной раскатке reload после readiness upstream обязателен: nginx
|
||||
разрешает Docker DNS при загрузке конфигурации и иначе может продолжить
|
||||
@@ -796,17 +871,17 @@ Expected:
|
||||
- Redis `PONG`;
|
||||
- Keycloak DB/realm/provider ready;
|
||||
- Collector health + exporter queue;
|
||||
- Safety ready и Redis DB2;
|
||||
- Safety v2 capability ready и Redis Safety (target); legacy DB2 проверяется только в stub acceptance;
|
||||
- API DB/Redis/JWKS/settings/S3/Safety ready;
|
||||
- local app до Bitrix install может быть `portal_not_installed`;
|
||||
- bitrix-sync возвращает `mode=db_connectivity_stub`, не CRM-ready;
|
||||
- bitrix-sync до enablement возвращает `sync_disabled`; после preflight — `mode=full`, validated settings/secrets/grants, живые worker/limiter и актуальные reconciliation cursors;
|
||||
- nginx config test success.
|
||||
|
||||
### Gate 12
|
||||
|
||||
- [ ] Все containers live, нет restart loop/OOM.
|
||||
- [ ] Critical readiness green.
|
||||
- [ ] Expected degraded statuses только Bitrix not-installed/sync stub.
|
||||
- [ ] Expected degraded statuses только документированные; sync stub mode отсутствует.
|
||||
- [ ] `docker compose ps` не публикует internal ports.
|
||||
- [ ] Internal `/internal/*` снаружи 404.
|
||||
- [ ] OTEL принимает telemetry.
|
||||
@@ -853,18 +928,22 @@ docker compose run --rm --no-deps <TOOLBOX_SERVICE> \
|
||||
|
||||
## 17. Stage 14 — public smoke и E2E
|
||||
|
||||
### 17.1. Edge
|
||||
### 17.1. Независимые public ingress
|
||||
|
||||
```bash
|
||||
curl -I http://<PUBLIC_HOST>/
|
||||
curl -fsS https://<PUBLIC_HOST>/api/v1/public/app-config
|
||||
curl -fsS https://<PUBLIC_HOST>/api/v1/public/content
|
||||
curl -fsS https://<PUBLIC_HOST>/auth/realms/han-chat/.well-known/openid-configuration
|
||||
curl -i https://<PUBLIC_HOST>/internal/safety/v1/messages/check
|
||||
curl -i https://<PUBLIC_HOST>/internal/safety/v2/messages/check
|
||||
openssl s_client -connect <PUBLIC_HOST>:443 -servername <PUBLIC_HOST>
|
||||
curl -I http://<PROCESSING_PUBLIC_HOST>/
|
||||
curl -i https://<PROCESSING_PUBLIC_HOST>/internal/sync/v1/status
|
||||
curl -i https://<PROCESSING_PUBLIC_HOST>/bitrix/sync/webhook/contact
|
||||
openssl s_client -connect <PROCESSING_PUBLIC_HOST>:443 -servername <PROCESSING_PUBLIC_HOST>
|
||||
```
|
||||
|
||||
Expected: 308; public 200 strict DTO; discovery 200; internal 404; valid cert.
|
||||
Expected ВМ1: 308; public 200 strict DTO; discovery 200; internal 404; valid cert. Expected ВМ2: HTTP redirect/ACME policy, internal 404, GET webhook 405/404, valid отдельный cert. Valid/invalid POST webhook проверяется отдельным form-urlencoded contract test с разрешённого и запрещённого source IP без помещения query token в shell history или логи.
|
||||
|
||||
### 17.2. Auth/frontend
|
||||
|
||||
@@ -883,7 +962,7 @@ Expected: 308; public 200 strict DTO; discovery 200; internal 404; valid cert.
|
||||
Обязательные E2E:
|
||||
|
||||
- text, начинающийся после normalization с `ф/Ф` → public `422 message_blocked`, Bitrix не вызван;
|
||||
- text с цифры → Safety `203`, API poll до `200` или test terminal `400`; клиент никогда не получает `203`;
|
||||
- legacy stub-only: text с цифры → v1 `203`/test terminal `400`; target v2 smoke использует `202`/`200|403|terminal 503`, клиент internal pending не получает;
|
||||
- прочий text → allow;
|
||||
- terminal stub `400` преобразуется в `422`, не в generic validation;
|
||||
- timeout → `503/504`, checkpoint/recovery, без duplicate;
|
||||
@@ -1196,8 +1275,11 @@ certbot delete active cert
|
||||
- managed PG private/TLS/backups/least privilege/migrations работают;
|
||||
- S3 private/IAM/CORS/lifecycle проверены;
|
||||
- exact release/images/frontend deployed;
|
||||
- `.env` validated, secrets protected;
|
||||
- один root Compose, один nginx, только 80/443;
|
||||
- non-secret `.env` validated; runtime secrets разделены по сервисам и защищены;
|
||||
- `deploy` не имеет Docker/root-equivalent доступа; production files root-owned, sudo ограничен конкретными systemd-units;
|
||||
- один root Compose и nginx на VM; ВМ1 и ВМ2 имеют независимые public 80/443, ВМ2 дополнительно private 8443; public ВМ2 ограничен exact CRM webhook;
|
||||
- container hardening и сетевые границы соответствуют arch-06;
|
||||
- для каждой private/no-egress VM завершён и задокументирован lockdown с негативной проверкой внешнего доступа/egress;
|
||||
- Redis/Collector volumes/resources/security работают;
|
||||
- Keycloak realm/provider/PKCE/OTP готов;
|
||||
- ordered startup/readiness пройден;
|
||||
@@ -1208,11 +1290,43 @@ certbot delete active cert
|
||||
- ops/incident/upgrade/DR owners назначены;
|
||||
- все assumptions/TBD приняты до открытия traffic.
|
||||
|
||||
### VM2 cutover, rollback, reprovision и Freshclam
|
||||
|
||||
Cutover gates: private TLS chain/SAN; Safety v2 PG migration и lease/fencing smoke; capability `text|links|files|worker`; S3 Gate 4; performance acceptance; egress negative tests. Safety Service Owner, Rule Pack Owner, Security Owner, Product Owner и Operations Owner фиксируют approvals. Только после них ВМ1 переключает `MESSAGE_SAFETY_URL`. Legacy v1 остаётся rollback target на ограниченное окно, но один `message_id` нельзя одновременно отправлять в v1 и v2.
|
||||
|
||||
Rollback возвращает caller adapter/upstream ВМ1 на предыдущий immutable release. Уже созданные v2 tasks завершаются/reconcile по PG checkpoints; down-migration и удаление quarantine versions запрещены.
|
||||
|
||||
При потере ВМ2 fail-open запрещён. ВМ2 reprovision-ится из immutable image/config; secrets materialize под отдельным IAM, Redis поднимается пустым, migrations/capability/egress gates повторяются. RTO ≤4 ч; restore rehearsal минимум дважды в год.
|
||||
|
||||
#### Message Safety config activation
|
||||
|
||||
Первая migration создаёт `message_safety.config_versions` и seed version 1; readiness не открывается без ровно одной valid active version. Config-only rollout выполняется отдельным root-owned migration/config job под config-admin DB role: создать immutable draft, проверить JSON Schema/cross-field constraints и наличие rules/detector artifacts, записать approvals, затем транзакционно activate. Runtime API/worker имеют только `SELECT` к config table.
|
||||
|
||||
После activation operator проверяет health `config_version`, text/link/file canaries, cache-key version и отсутствие изменения in-flight task version. Изменение, ослабляющее policy или увеличивающее limits, требует Security Owner; остальные — Safety Service Owner и Operations Owner. Rollback не реактивирует retired row: предыдущий payload клонируется в новую monotonic version и активируется с отдельным audit event.
|
||||
|
||||
#### Emergency MOCK
|
||||
|
||||
`deploy` может без root login включить/изменить/выключить mode:
|
||||
|
||||
```bash
|
||||
sudo /usr/local/sbin/han-message-safety-mode mock --text-free true --file-free true
|
||||
sudo /usr/local/sbin/han-message-safety-mode mock --text-free true --file-free false
|
||||
sudo /usr/local/sbin/han-message-safety-mode standard
|
||||
```
|
||||
|
||||
Root устанавливает helper и `/etc/sudoers.d/deploy-message-safety-mode` при bootstrap. Sudoers разрешает `deploy` только этот immutable `root:root 0755` executable; helper имеет строгий parser без shell eval/path arguments, атомарно обновляет `root:han-message-safety 0640` `/etc/han-chat/message-safety-mode.env` (dedicated GID `10001` доступен только non-root контейнеру), валидирует Compose config, выполняет фиксированную Message Safety API recreate/restart operation внутри root project и проверяет private health. На ошибке он восстанавливает предыдущий mode/config и повторяет restart. `deploy` не получает write к config, systemd units, Compose и Docker socket.
|
||||
|
||||
Перед включением operator фиксирует incident/change ID и выбранные text/file policies; после команды проверяет `processing_mode=mock`, forced canaries, отсутствие `202`, metric/active alert и audit actor. Ранее принятые standard tasks сохраняют mode и завершаются без переклассификации; только новые requests используют MOCK. Автоматического timeout нет: mode действует без ограничения по времени до явного `standard`. Поэтому перед закрытием incident обязателен возврат в standard, проверка normal capabilities и text/link/EICAR canary. File, разрешённый в MOCK, маркируется `scan_status=bypassed`, а не `clean`.
|
||||
|
||||
`freshclam` имеет controlled egress только к утверждённому signature CDN. Max signature age 24 ч; stale/failed update выключает только `files` и поднимает alert. Новая база проходит integrity/load/EICAR canary и atomic activate/reload; при regression возвращается последняя валидная база.
|
||||
|
||||
Initial ВМ2: 4 vCPU/8 GiB/80 GiB, resource/PID limits и backpressure. Workers масштабируются первыми по queue depth, `clamd` — scan lanes. ВМ3/scale-out инициируются при sustained CPU/RAM >70%, queue age >30 с, провале module-05 performance gates, contention `bitrix-sync` или независимом release cadence.
|
||||
|
||||
## 28. Допущения, TBD и архитектурные конфликты
|
||||
|
||||
### Допущения
|
||||
|
||||
- D-A1: одна VM и один public host на MVP.
|
||||
- D-A1: отдельные public hosts ВМ1/ВМ2; processing host публикует только CRM webhook, остальные сервисы ВМ2 остаются private.
|
||||
- D-A2: обязательный минимум — `otel-collector`; доступность remote backend не предполагается до закрытия D-TBD11, local Grafana stack не обязателен.
|
||||
- D-A3: managed provider даёт private network, TLS, backups/PITR.
|
||||
- D-A4: Bitrix portal/connector/line остаются разрешёнными значениями architecture.
|
||||
@@ -1221,25 +1335,25 @@ certbot delete active cert
|
||||
### TBD до production
|
||||
|
||||
- D-TBD1: реальные domains, Expo native redirect URI и Bitrix placement frame ancestors.
|
||||
- D-TBD2: final VM/PG sizing, RPS/WS, SLO/RPO/RTO.
|
||||
- D-TBD2: final VM1/PG sizing, public API/WS SLO и общие RPO/RTO; Safety load/latency gates уже зафиксированы, monthly availability SLO Safety в MVP намеренно не вводится.
|
||||
- D-TBD3: legal retention/erasure для PG/S3/audit/telemetry.
|
||||
- D-TBD4: secret manager и rotation windows.
|
||||
- D-TBD5: production Safety вместо stub и antivirus inbound operator files.
|
||||
- D-TBD6: полноценный bitrix-sync/GRANT или явное исключение CRM sync из release.
|
||||
- D-TBD5: реализовать/cutover production Safety v2; inbound operator files сознательно не проходят AV в MVP.
|
||||
- D-TBD6: реализовать код, migrations, portal fields/smart process/webhooks и выполнить bitrix-sync cutover по module-07.
|
||||
- D-TBD7: pinned Keycloak/nginx/Collector versions и SPI compatibility.
|
||||
- D-TBD8: exact migration/seed/toolbox CLI commands после реализации repo.
|
||||
- D-TBD9: Keycloak admin VPN/MFA topology.
|
||||
- D-TBD10: final CSP/CORS/S3 headers и cloud-specific IAM.
|
||||
- D-TBD11: выбрать observability backend/provider, endpoint/auth/retention и alert route либо явно ограничить среду acceptance-режимом без production-ready SLO.
|
||||
- D-TBD11: уточнить auth/retention/alert route выбранного private SigNoz; backend и endpoint уже зафиксированы.
|
||||
|
||||
### Обнаруженные конфликты
|
||||
|
||||
1. `arch-01/02/03` описывают полноценный `bitrix-sync`, но module-07 реализует только `SELECT 1`. Первый release не поддерживает обещанную CRM profile sync.
|
||||
2. `arch-01/02` ожидают production-like Safety и S3 scan, но module-05 — Redis-only random stub, file-only default allow и test-only terminal `400`. Это блокер настоящего production, даже если допустимо для production-like acceptance.
|
||||
1. `module-07` теперь задаёт полный target `bitrix-sync`; до реализации кода/migrations/portal prerequisites сервис обязан оставаться disabled, документация сама по себе не означает выполненный cutover.
|
||||
2. Текущая implementation остаётся v1 stub; module-05 описывает draft target v2. Cutover является production gate.
|
||||
3. Прототипный PG init даёт runtime role `CREATE` schema и не разделяет migration/runtime roles; runbook требует ужесточения.
|
||||
4. Прототипные TLS scripts используют отдельный service Compose/standalone downtime, тогда как целевая архитектура требует root Compose и two-phase webroot.
|
||||
5. Prototype публиковал `/bitrix-internal/*` и использовал `/internal/v1/*`; целевой контур это запрещает и использует `/internal/openlines/v1/*`.
|
||||
6. `arch-04` не содержит ряд proposed env из module-04–09; production `.env.example` должен быть синхронизирован до реализации.
|
||||
6. Runtime artifacts `.env.example`/Compose ещё могут не содержать зафиксированные VM2 env; документация не означает выполненный cutover.
|
||||
7. Точные RPO/RTO, SLO, Keycloak version и Bitrix retry semantics не утверждены; OTP TTL задаётся `app_settings`, SMS journal по module-11 хранится бессрочно.
|
||||
8. `init-managed-postgres.py` по умолчанию не задаёт TLS parameters при bootstrap connection и печатает credential-bearing DSN; его production-hardening обязателен.
|
||||
9. Текущие Compose/env/config artifacts могут ещё не содержать `sms-service`; документация не разрешает real mode до реализации и прохождения rollout gates.
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
> Статус: исходный концепт и история обсуждения. Каноническая реализационная постановка после принятия решений — [`module-07-bitrix-sync.md`](module-07-bitrix-sync.md). При расхождении применяется module-07.
|
||||
>
|
||||
> Актуализация 2026-08-06: production-проверка reconciliation использует `crm.item.list`, `entityTypeId=3`, фильтры `>=updatedTime`, `opened=1`, `ufCrm_1778692456=1`. Указанные ниже ранние варианты `UF_CRM_6A70C275346A7`, `Y/N` и поиск по двум телефонным маскам сохранены только как история и не являются реализационным контрактом.
|
||||
|
||||
MCP Server Bitrix24 с документацией https://mcp-dev.bitrix24.tech/mcp
|
||||
|
||||
### 1. Границы релиза
|
||||
1.1. Что входит в **первый** релиз sync:
|
||||
`contact.map_or_create`, `contact.update`,
|
||||
передача в Битрикс24 тэга о том, что пользователь зарегистрирован в приложении
|
||||
webhook Bitrix→App при изменении данных у пользователей, зарегистрированных в приложении
|
||||
обновление сведений о данных пользователя при изменении их в Битрик24,
|
||||
ведение бизнес-логов с конфликтами и ошибкам, требующих внимания.
|
||||
создание и отслеживание статуса alerts по конфликтам
|
||||
|
||||
1.2. Что явно **вне scope**:
|
||||
Lead/Deal,
|
||||
документы компании в профиль,
|
||||
merge контактов,
|
||||
ручной replay API
|
||||
|
||||
1.3. Можно ли выпускать без двусторонности (только App→Bitrix), или webhook обязателен сразу?
|
||||
вебхук обязателен в первой сборке
|
||||
|
||||
### 2. Сущности и маппинг полей
|
||||
2.1 Какие поля `ClientProfile` / `UserIdentity` синхронизируем (в скобках поле в битрикс24):
|
||||
`full_name` ↔ name
|
||||
`citizenship` ↔ ufCrm_1768493029,
|
||||
`russian_phone` ↔ phone,
|
||||
`email` ↔ email
|
||||
|
||||
Значение citizenship возвращается ИД. Справочник Битрикс можно загрузить
|
||||
curl -sS -G 'https://<portal-name>/rest/<user-id>/<token>/crm.contact.userfield.list' \
|
||||
--data-urlencode 'filter[FIELD_NAME]=uf_crm_1768493029'
|
||||
|
||||
При запросах важно соблюдать следующий принцип: сервис проектируется таким образом, чтобы запрашивать минимум необходимой информации.
|
||||
Т.е.
|
||||
а) если мы синхронизинуем 5 полей, то мы запрашиваем ровно 5 нужных полей. Не запрашиваем все данные по клиенту.
|
||||
б) если нам надо синхронизировать контакт по телефону - мы ищем в Б24 контакт только по этому телефону. Не запрашиваем список контактов.
|
||||
|
||||
Второй момент, выявленный в ходе тестирования:
|
||||
Номер телефона может храниться в двух форматах - "+7xxxxxxxxxx", "+7 (xxx) xxx-xxxx"
|
||||
Соответственно, по каждому искомому телефону делаем запрос по двум маскам, указанным выше.
|
||||
|
||||
Также сервис следует проектировать с учетом ограничений Битрикс24 по кол-ву обращений к АПИ (генерируем очередь и делаем запросы по расписанию через batch).
|
||||
|
||||
Пример запроса требуемых данных для одного пользователя:
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-d '{"entityTypeId":3,"select":["id","name","phone","ufCrm_1768493029","createdTime"],"filter":{"@phone":["+7xxxxxxxxxx","+7 (xxx) xxx-xxxx"],"opened":"Y"}}' \
|
||||
https://<portal-name>/rest/<user-id>/<token>/crm.item.list
|
||||
|
||||
2.2. Нужен ли флаг «контакт зарегистрирован в приложении» в Bitrix, и в каком поле?
|
||||
Нужен (поле UF_CRM_6A70C275346A7: Y/N).
|
||||
|
||||
|
||||
### 3. Правила матчинга Contact
|
||||
3.1. Ключ поиска: только телефон (как выше указывал телефон хранится по одной из двух масок; возможно в документации есть более стабильные методы поиска контакта по номеру телефона)
|
||||
|
||||
3.2. Управление конфликтами:
|
||||
Для разбора конфликтов должны создаваться alert: задача в Б24 (смарт процесс "Конфликты синхронизации") и бизнес-лог в БД с типом проблемы, деталями, ссылкой на ИД задачи в Б24 и ее текущим статусом.
|
||||
Жизненный цикл alert: alert создается СС, обработка alert осуществляется в Б24 через смарт процесс "Конфликты синхронизации", БД регулярно опрашивает статус задач. После успешного разбора конфликта, СС корректирует статус в БД на "Завершено". ИД alerts нумеруются сиквенсом и передаются в Б24 при постановке задачи.
|
||||
Описанный механизм разбора конфликтов - используется для разбора бизнес-расхождений. Он не используется в случае технических сбоев или ошибок приложения.
|
||||
|
||||
3.3. Жизненный цикл пользователя приложения в контексте связи с контактом (на примере 1 пользователя):
|
||||
а) Клиент зарегистрировался по номеру телефона.
|
||||
СС запрашивает в Б24, есть ли клиенты с указанным номером телефона.
|
||||
Варианты:
|
||||
В0. 0 контактов. СС должен инициировать create, получить информацию об ИД нового пользователя, записать к себе ИД в Б24, в Б24 передать флаг привязки к Приложению.
|
||||
В1.1. 1 контакт + контакт не имеет связи с приложением. СС должен записать к себе ИД в Б24, в Б24 передать флаг привязки к Приложению. Если при этом в приложении флаг уже был, нам не важно.
|
||||
В1.2. 1 контакт + контакт уже привязан к другому активному пользователю приложения. СС должен инициировать create, получить информацию об ИД нового пользователя, записать к себе ИД в Б24, в Б24 передать флаг привязки к Приложению. Плюс СС создает alert об ошибке привязки.
|
||||
В2.1. 2 и более. СС выбирает самый новый контакт по creationtime. Выбранный контакт не имеет связи с приложением. СС должен записать к себе ИД в Б24, в Б24 передать флаг привязки к Приложению. Плюс СС создает alert наличии дублей контактов: необходимо проверить актуальность контактов, корректность закрепления.
|
||||
В2.2. 2 и более. СС выбирает самый новый контакт по creationtime. Выбранный контакт уже привязан к другому активному пользователю приложения. СС должен инициировать create, получить информацию об ИД нового пользователя, записать к себе ИД в Б24, в Б24 передать флаг привязки к Приложению. Плюс СС создает alert об ошибке привязки: с указанием деталей события (в том числе информация о наличии нескольких контактов).
|
||||
|
||||
б) Процесс обновления данных.
|
||||
Сейчас реализовываем только движение Б24 -> Приложение.
|
||||
Б24 отслеживает изменения данных контактов, у которых есть признак UF_CRM_6A70C275346A7 = 'Y'
|
||||
При изменении Б24 направляет вебхук СС
|
||||
СС при получении вебхука складывает в очередь задание на обновление данных соответствующего контакта.
|
||||
По расписанию выполняется запрос данных контактов из Б24. И обновляются сведения. Если в ответ на запрос по контакту не найдены данные, формируется alert (Пользователь приложения привязан к несуществующему контакту).
|
||||
|
||||
в) Клиент удалил профиль из приложения.
|
||||
Обновляем признак UF_CRM_6A70C275346A7 = 'N'. Деактивируем учетную запись.
|
||||
|
||||
Механизм изменения привязки ИД Битрикс24 к пользователю приложения - администратор вносит изменения напрямую в БД
|
||||
|
||||
### 4. Очередь и worker
|
||||
|
||||
4.1. Этот пункт - примерное видение. По нему можно смело предлагать улучшения, тк. получается несколько связанных асинхронных процессов.
|
||||
|
||||
В схеме han_app хранятся бизнесовые задачи на обновление данных. Должен быть воркер, который забирает эти задачи и стартует требуемые сценарии (map_or_create, update, delete, alert_create, alert_status_update).
|
||||
|
||||
Каждый сценарий предполагает набор действий и запросов в Б24. Обработчик сценария в рамках его исполнения формирует задачи на обмен данными с Б24 (один сценарий может предолагать несколько связанных задач). В следующем пункте расписал примерные запросы в Б24, которые могут возникать в ходе сценария. Тебе нужно более подробно сформулировать сценарий и действия.
|
||||
|
||||
Задачи на обмен с Б24 складываются в соответствующую таблицу в схеме sync. Обмен данными с Б24 осуществляется через механизм Батчей. Кроме операций синхронизации документов (не входят в текущую реализацию). Один вложенный в батч запрос должен относиться к одной задаче синхронизации.
|
||||
Отправка запроса осуществляется по шедулеру (по умолчанию каждые 5 секунд). Для этого отдельный воркер собирает задачи, находящиеся в статусе pending + кол-во tryes <max_retries (не более 20 штук в один батч). Задачи отбираются по принципу fifo. Если задач меньше 20, то батч формируется из меньшего числа задач. Если задач 0, то батч не формируется, запрос в битрикс не отправляется.
|
||||
Отобранные задачи переводятся в следующий статус (модель статусов предложи сам). После получения ответа успешные ответы переходят на следующий статус, неуспешные ответы, требующие retry, возвращаются в статус pending, кол-во tries + 1. Успешный ответ, касающийся задачи, сохраняем в таблице с задачами.
|
||||
Триггер (или шедулер) проверяет наличие задач в статусе pending + кол-во tries >=max_retries. Если находит, переводит в статус fail + отбрасывает бизнес-лог для разбора (в Битрикс24 задача не создается).
|
||||
Значения параметров прописываются в настройках сервиса в БД в схеме sync_service, изменения параметров в БД должны применяться сервисом без перезагрузки.
|
||||
|
||||
Какие у меня архитектурные сложности возникли, ограничивающие возможность полноценно поставить требования:
|
||||
1) Битрикс24 допускает до 5 api запросов в секунду. По идее при высокой нагрузке это может означать что регулярность шедулера можно настроить до 0,2 секунд. Я не понимаю, нужно ли тут какой-то параллелиризм вводить? или БД с одним шедулером справится, тк Б24 достаточно быстро отвечает. Но что произойдет, если Б24 за 0.2 секунды не ответит? Тут нужна твоя экспертиза и варианты.
|
||||
2) Как правильно организовать работу сценариев. Сценарий может содержать несколько последовательных действий, требующих обмена с Б24 и не требующих. Жизненный цикл отработки сценария начинается с момента, как я забрал задачу из han_app, или ее поставил сам СС. Держать в оперативной памяти сценарий на протяжении его жизненного цикла неправильно, тк очередь задач может забиться и либо все рухнет, либо сценарии перестанут запускаться - формируется точка отказа. Правильнее сценарий разделить на условно-атомарные операции, и раскладывать их в таблицу. И тогда какие-то воркеры могут быстро бегать по этой таблице, находить очередную операцию в рамках сценария, которая ждет исполнения, исполнять ее, переводить в статус "исполнена", чтобы активировать к возможности исполнения следующую задачу. Мне этот вариант кажется более отказоустойчивым и управляемым (плюс в любой момент, даже если грохнется сервис, можно запуститься с того места, где он грохнулся, и никакие сценарии не оборвутся). Но я не до конца понимаю, как тут управлять производительностью (можно ли много воркеров запускать, чтобы они параллельно работали, и в какой момент это нужно делать).
|
||||
|
||||
4.2. Виды запросов в Б24 с привязкой к сценариям:
|
||||
Сценарий регистрации пользователя:
|
||||
1: поиск контакта по номеру телефона
|
||||
2: создание нового контакта либо
|
||||
3: обновление сведений контактов (изменений флага UF_CRM_6A70C275346A7)
|
||||
|
||||
Сценарий обновления
|
||||
4: запрос данных по контакту
|
||||
|
||||
Сценарий удаления профиля:
|
||||
3: обновление сведений контактов (изменений флага UF_CRM_6A70C275346A7)
|
||||
|
||||
Сценарий обработки alerts:
|
||||
5: поставить задачу в Б24
|
||||
6: узнать статус задачи в Б24
|
||||
|
||||
... возможно еще понадобятся.
|
||||
Используемые виды запросов хранятся в справочнике видов запросов и могут использоваться сервисом синхронизации в рамках запущенных процессов.
|
||||
|
||||
4.3. Сервис синхронизации должен ставить задачи на синхронизацию с использованием утвержденных видов запросов в Б24.
|
||||
|
||||
4.4. Готовы ли триггеры App DB и GRANT для `bitrix_sync_user` , или это часть той же постановки?
|
||||
Триггеры 'contact.map_or_create' и 'contact.update' при создании и изменении данных в таблицах пользователей готовы и работают. Действующие триггеры необходимо проверить, что они не будут пытаться синхронизировать изменения, полученные от битрикс24 и залитые в БД. Процессов постановки задач на обновление данных пользователя из-за вебхука от Б24, нет.
|
||||
|
||||
### 5. Bitrix24: доступ и webhook
|
||||
5.1. Способ доступа к CRM REST: я верно понимаю, что local-app безопаснее входящего вебхука? Т.к невозможно, даже зная токен, отправить запрос в Б24? Если это так, то логично создать в Б24 еще одно локальное приложение и получать данные через него. Для вебхуков от Б24 делаем соответствующую ссылку с секретом.
|
||||
5.2. Кто настраивает робота в Bitrix (какие события/поля). Сформируй требования к роботу и исходящему вебхуку, я настрою робот.
|
||||
5.3. Есть ли тестовый портал отдельно от prod. Нет, портал один. При этом для разделения теста и прода будут использоваться различные local-app. Чтобы не смешивать базу, набор тестовых пользователей будет задаваться маской "нелегитимных" номеров.
|
||||
|
||||
### 6. Надёжность, безопасность, ops
|
||||
6.1. Rate limit / квоты Bitrix REST: политика при `QUERY_LIMIT_EXCEEDED`?
|
||||
Кол-во попыток определено в п.4.1. Если не удалось - отбрасываем бизнес-логи. Правило универсально как для ошибок синхронизации, так и для QUERY_LIMIT_EXCEEDED. Можешь предложить вариант лучше, если есть идеи.
|
||||
6.2. PII в логах/метриках: секреты в логах не допускаются. PII в логах маскируются: два первых читаемых и два последних читаемых символа поля отображаем как есть, остальные символы маскируем. Ошибки логируем как есть - т.к. сервис внутренний и недоступен пользователям.
|
||||
6.3. Cutover: stub → full sync на уже накопленной очереди — replay всех pending или только новых? Управляем через .env: replay = true, значит replay всех pending. replay = false, значит все pending при раскатке сервиса переводим в статус "Cancelled".
|
||||
|
||||
Reference in New Issue
Block a user