Сценарий: подключение клиентской credential поставщика
NOTE
Статус: Target design. Документ описывает целевую доменную модель. Соответствующий код реализован частично (см.
backend/internal/core/) или пока не начат. Правила маркировки — в50-processes/documentation-standard.md.
Триггер
Customer добавляет учётную запись поставщика через UI кабинета.
Участники
| BC | Роль |
|---|---|
| Customer | Owner aggregate; ссылается на credentials. |
| Credentials | Owner процесса. Шифрование, validation, fingerprint, group merge. |
| Ingestion | Подписан на SupplierCredentialActivated → запускает discovery. |
| Pricing | Invalidate cache при изменениях. |
| Visibility | Применяет дефолты для нового context. |
| External (KMS) | Через MasterKeyProvider. |
Sequence diagram
sequenceDiagram autonumber participant U as 🟫 Customer participant API as Customer Cabinet API participant CR as Credentials participant KMS as 🌐 KMS / Vault participant CON as Connector (Ingestion) participant SUP as 🌐 Supplier API participant ING as Ingestion participant P as Pricing U->>API: POST /credentials {supplier=etm, schema=login_password, login, password} API->>CR: 🟦 AddCredential CR-->>CR: 🟧 SupplierCredentialProvided{status=draft} Note over CR: 🟪 Encrypt at rest CR->>KMS: get DEK (per-customer) KMS-->>CR: DEK CR->>CR: encrypt secret fields CR-->>CR: 🟧 CredentialEncryptedAtRest CR-->>CR: 🟧 status=validating CR->>CON: ValidateCredential CON->>SUP: test login + minimal request alt success SUP-->>CON: 200 CON-->>CR: 🟧 SupplierCredentialValidated CR->>CR: 🟦 ComputeFingerprint CR->>CR: lookup SupplierCredentialGroup by fingerprint alt совпал, autoMerge условия выполнены CR-->>CR: 🟧 SupplierCredentialGroupMerged Note over CR: используется existing primary credential для опросов else совпал partial / условия не выполнены CR-->>CR: 🟧 CredentialGroupCandidateOpened (moderation) else fingerprint новый CR-->>CR: 🟧 SupplierCredentialGroupCreated{primary=this} end CR-->>CR: 🟧 SupplierCredentialActivated CR-->>ING: 🟧 SupplierCredentialActivated CR-->>P: 🟧 SupplierCredentialActivated ING->>ING: 🟦 EnqueueEnrichmentJob{kind=discovery} P-->>P: invalidate pricing cache for this customer else auth_failed SUP-->>CON: 401 CON-->>CR: 🟧 SupplierCredentialFailing Note over CR: notify customer, badge в кабинете end API-->>U: 200 + status
Шаги
- Customer вводит данные — UI рендерится по JSON Schema схемы поставщика; поля с
secret: trueпомечены. - API → AddCredential — Credentials BC создаёт
SupplierCredential{status=draft}. - Encrypt at rest (Policy):
- Получить per-customer DEK (через
MasterKeyProvider). - Шифровать secret-поля.
- Сохранить в PG.
CredentialEncryptedAtRest.
- Получить per-customer DEK (через
- Validate — Connector выполняет provider contract validation suite (ADR-0023 + ADR-0024):
- auth success (login → получен session);
- minimal data fetch (для ETM:
GET /goods/{any_etm_code}/price?type=etm— проверяет и data rate limit, и корректность session); Capabilitiesматчит то, что поставщик реально отдаёт (если декларированоPrices=true, price endpoint должен ответить). Любой fail →ConnectorError→ не меняем status:AuthRejected→SupplierCredentialFailing+ notification клиенту.SessionExpiredна первом же запросе — bug connector’а → alert, credential остаётсяvalidatingдля ручного разбора.- Success suite →
SupplierCredentialValidated.
- Compute fingerprint:
fingerprint = sha256(supplier_id + auth_schema + canonical_identity_field(payload)).canonical_identity_field:login_password→ normalized login (lowercase + trim).api_key→ hash или prefix.oauth2→ provider-specific user id (получается при validation).bearer_token→ hash.
- Group lookup:
SELECT group WHERE fingerprint = ?.- Найдена + auto-merge условия выполнены (см. ниже) →
SupplierCredentialGroupMerged. - Найдена + partial / условия не ок →
CredentialGroupCandidateOpened→ moderation queue. - Не найдена →
SupplierCredentialGroupCreated{primary=this credential}.
- Activate —
SupplierCredentialActivated. Публикуется как integration event. - Downstream:
- Ingestion: запуск
EnrichmentJob{kind=discovery}с этой credential — что новое поставщик может предложить. - Ingestion (если connector имеет
dictionary_exportscapability):EnrichmentJob{kind=client_sku_map_import}— импорт cross-reference клиентских SKU. Для ETM — черезSgGdsasync-report с последующим парсингом(id, cli_code, article, brand_code). - Pricing: invalidate cache для customer. Будущие запросы будут использовать эту credential.
- Ingestion: запуск
- UX: customer видит status в кабинете; история использования (запросов от его имени).
Auto-merge условия (для group)
Все должны выполниться:
- Полное совпадение
(supplier_id, auth_schema, fingerprint). - Обе credentials —
active(или однаvalidating, только что создана). - Customer’ы — разные (для одного customer объединение бессмысленно).
- Существующая primary credential успешно валидировалась последние 7 дней.
При partial совпадении (тот же логин, разный пароль; тот же API key, разный key_id) — НЕ auto-merge.
Decision points
- Customer вводит дубль своей же credential — отказ с сообщением «уже есть».
- Fingerprint совпадает с другим customer, но schema validation failed для этого customer’а — credential остаётся
failing, в группу не входит. - Customer пытается revoke single credential, а это primary в группе — group переключается на следующего active member; если их нет — group → archived.
- Customer revoke во время активного pricing-запроса — request завершается с пометкой
customer_credential_revoked(fallback на system).
Edge cases
| Случай | Поведение |
|---|---|
| KMS недоступен в момент создания | 503 + retry; credential не создаётся (без шифрования — нельзя). |
| Connector validation timeout | SupplierCredentialFailing + notification. Customer может re-validate. |
| Customer вводит OAuth credential — провайдер просит consent | Redirect к provider’у; callback завершает enrichment payload. |
| Поставщик rotate’ит pass on his side | Auth fail → SupplierCredentialFailing. Customer уведомлён, обновляет в кабинете. |
| Группа имеет 5 members, primary стал failing | Один из members (next active) становится primary. Остальные продолжают видеть observations с переводом ownership. |
| Holding clients имеют одну учётку (Альфа-Север + Альфа-Юг на ETM) | Обе credentials → одна group; physical опрос ETM — один; обе видят observations; pricing rules могут различаться. |
Инварианты сценария
- Plaintext секрета никогда не попадает в логи / errors / responses.
- После
SupplierCredentialProvidedбезCredentialEncryptedAtRest— credential нельзя активировать (state machine). - Privacy: customer A не видит, что customer B — member той же group (administrative дедупликация прозрачна).
- Discovery job после activate —
kind=discovery, чтобы не дублировать observations при group-merge. - Любая
DecryptForUseлогируется вcredential_decrypt_auditсreason.
Метрики и observability
credentials_added_total{supplier, scope}.credentials_validation_latency_seconds{supplier}.credentials_validation_failures_total{supplier, reason}.credential_group_merges_total{supplier}.credential_decrypts_total{reason}— для аудита.
Связанные файлы
- Контексты:
../contexts/credentials.md,../contexts/customer.md,../contexts/ingestion.md,../contexts/pricing.md. - Сценарии:
customer-auth.md,pricing-calculation.md. - ADR-0010 (pluggable credential schemas + envelope encryption).
- ADR-0018 (master key и DEK rotation).