Контекст: учётные записи

NOTE

Статус: Target design. Документ описывает целевую доменную модель. Соответствующий код реализован частично (см. backend/internal/core/) или пока не начат. Правила маркировки — в 50-processes/documentation-standard.md.

Назначение

Proposal pipeline (search-proposal.md, Layer 1.2) использует CredentialRouter.Select. Precedence customer → system разрешается один раз per briefing с учётом CredentialConstraints (manufacturer_scope/classification_tag_scope/warehouse_scope). После выбора все чтения идут по exact credential_ref. См. §4.2.1 design spec + ADR-0035 §13.19.

Управляет учётными записями доступа к API поставщиков: системными и клиентскими. Шифрует секреты, дедуплицирует совпадающие учётки в SupplierCredentialGroup, валидирует, отслеживает lifecycle. Выдаёт CredentialContext (Shared Kernel) для Ingestion и Pricing.

Главный смысл

Мы — не прокси к поставщику (P17, ADR-0011). Credentials используются background-процессами для пополнения собственного хранилища. Один и тот же логин у разных клиентов = одна physical-фактура опроса (через SupplierCredentialGroup). Privacy invariant — observation от credential client’а A не утечёт client’у B.

Агрегаты / сущности / value objects

ИмяТипНазначение
SupplierCredential🟨 AggregateУчётка. Корень: credential_id.
CredentialScopeVO enumsystem / customer.
AuthSchemaVO enumlogin_password / api_key / oauth2 / bearer_token / master_key_token_exchange / public_no_auth / custom. master_key_token_exchange — долгоживущий MasterKey → обмен на короткий access_token (DKC pattern). public_no_auth — открытый file feed (КЭАЗ).
TokenExchangePayloadVOДля master_key_token_exchange: {master_key (encrypted), last_token?, last_token_valid_until?, exchange_endpoint}. Token refresh — автоматический через SessionManager.
AuthPayloadVOJSONB; secret: true поля шифрованы.
AuthMetadataVOОткрытые поля.
FingerprintVOsha256(supplier_id + auth_schema + canonical_identity_field(payload)).
SupplierCredentialGroupEГруппа credentials с одинаковым fingerprint.
CredentialFeaturesVO enum-setcatalog / prices / stock / place_orders / media_unwatermarked / dictionary_exports / async_reports.
CredentialConstraintsVO(warehouse_scope?, classification_tag_scope?, manufacturer_scope?, egress_policy?) — ограничения, выставленные поставщиком на данную credential.
EgressPolicyVO(fixed_ip_required, allowed_cidrs?) — для поставщиков, требующих whitelisted source IP (например, ETM для foreign hosting).
CredentialStatusVO enumdraft / validating / active / failing / expired / revoked.
CredentialContextVOShared Kernel — передаётся в Connector. Несёт features, constraints, ключи AuthBucket и DataBucket для rate limiter.
MasterKeyProvider⚙️ PortVault Transit / KMS / sealed-file.
DEKVOPer-customer Data Encryption Key (зашифрован master key).
RateLimitOverridesVOPer-credential overrides по {auth: {...}, data: {endpoint: {...}}}.
WarehouseScopeVOСписок доступных складов (подмножество графа Supplier Network).
ClientSkuMap🟨 AggregateCross-reference клиентских SKU на коды поставщика. Owner: Credentials BC (тесно связан с credential). Идентификация: (customer_ref, supplier_ref, credential_ref).
ClientSkuMappingVO(client_sku, supplier_sku, manufacturer_article?, verified_at, source).
ClientSkuMappingSourceVO enummanual_csv / dictionary_export / observed_in_payload / operator.
CredentialDecryptAuditEntryEappend-only запись расшифровки.

Доменные события

СобытиеПричина
УчётнаяЗаписьПоставщикаПередана (SupplierCredentialProvided)Customer добавил, scope=draft
УчётнаяЗаписьЗашифрована (CredentialEncryptedAtRest)Policy: при создании — обернуть DEK через KMS
УчётнаяЗаписьПроверена (SupplierCredentialValidated)Connector: тестовый запрос успешен
УчётнаяЗаписьАктивирована (SupplierCredentialActivated)Validating → active
УчётнаяЗаписьСбоит (SupplierCredentialFailing)N подряд auth-ошибок
УчётнаяЗаписьИстекла (SupplierCredentialExpired)valid_to прошло
УчётнаяЗаписьОтозвана (SupplierCredentialRevoked)Customer / Operator
ГруппаУчётныхЗаписейСоздана (SupplierCredentialGroupCreated)Первая credential данного fingerprint
ГруппаУчётныхЗаписейОбъединена (SupplierCredentialGroupMerged)Совпал fingerprint, autocomplete merge
ОбъединениеТребуетПроверки (CredentialGroupCandidateOpened)Частичное совпадение → moderation
СекретРасшифрован (CredentialDecrypted)Любое использование секрета (audit-обязательно)
BreakGlassОперацияВыполнена (CredentialBreakGlassPerformed)4-eyes расшифровка для investigation
DEKПовёрнут (DekRotated)Плановая / aviation ротация
MasterKeyПовёрнут (MasterKeyRotated)Compromise / scheduled

Команды

КомандаАкторЦелевой агрегатРезультат
ДобавитьУчётнуюЗапись (AddCredential)Customer / OperatorSupplierCredentialSupplierCredentialProvided + CredentialEncryptedAtRest
ПроверитьУчётнуюЗапись (ValidateCredential)WorkerSupplierCredentialSupplierCredentialValidated или SupplierCredentialFailing
АктивироватьУчётнуюЗапись (ActivateCredential)PolicySupplierCredentialSupplierCredentialActivated
ОтметитьСбой (MarkFailing)ConnectorSupplierCredentialSupplierCredentialFailing
ОтозватьУчётнуюЗапись (Revoke)Customer / OperatorSupplierCredentialSupplierCredentialRevoked
ВычислитьFingerprint (ComputeFingerprint)Engine
ПрисоединитьКГруппе (MergeIntoGroup)PolicySupplierCredentialGroupSupplierCredentialGroupMerged
РасшифроватьДляИспользования (DecryptForUse)Connector / PricingCredentialDecrypted (audit)
BreakGlassРасшифровка (BreakGlassDecrypt)Security (4-eyes)CredentialBreakGlassPerformed
ПовернутьDEK (RotateDek)Schedule / IncidentDekRotated

Политики

ТриггерРеакция
SupplierCredentialProvidedEncryptAtRest (DEK через master key)
CredentialEncryptedAtRestValidateCredential (тестовый запрос согласно provider contract tests ADR-0023)
SupplierCredentialValidatedComputeFingerprint, поиск группы
Fingerprint match + все условия auto-mergeMergeIntoGroup
Fingerprint partial match→ publish CredentialGroupCandidateOpened → AI-агент credential_group_candidate в Moderation BC (видит метаданные, никогда секреты)
ProposedAction(MergeCredentialGroup) от ModerationMergeIntoGroup + ack DecisionApplied
SessionExpired (Ingestion)не инкрементирует счётчик failing. SessionManager запускает re-login.
SessionAuthFailed (AuthRejected после свежего login) 3+ за 10 минMarkFailing
SupplierCredentialFailing→ notification клиенту, badge в кабинете
Customer renewed credentialValidateCredential снова
CustomerCredentialActivated + connector имеет dictionary_exports capabilityEnqueueEnrichmentJob{kind=client_sku_map_import} для пополнения ClientSkuMap
Schedule (180 дней)RotateDek per-customer
Compromise master keyRotateDek для всех (24ч)

Шифрование

См. ADR-0010 (envelope encryption) и ADR-0018 (master key и DEK rotation policy).

  • Секретные поля (secret: true) шифруются app-level: master key оборачивает per-customer DEK; DEK шифрует поля credential.
  • MasterKeyProvider — Vault Transit / KMS / sealed-file (prod), env-plain (только dev с явным флагом).
  • Каждая зашифрованная запись хранит master_key_id — позволяет вращать без single-shot миграции.
  • В логах секреты redacted на уровне logger middleware (поля с secret: true автоматически вырезаются).

Read-модели

  • 🟩 customer_credentials_dashboard (PG) — для customer кабинета.
  • 🟩 credential_groups_view (PG) — для админки.
  • 🟩 credential_decrypt_audit (CH) — все расшифровки.
  • 🟩 connector_credential_metrics (CH) — успехи / auth_failed / rate_limited.

Инварианты

  1. scope = customercustomer_ref не пуст.
  2. Сами секреты (secret: true) хранятся только в зашифрованном виде. Plaintext не попадает в логи / PG snapshots / errors.
  3. fingerprint детерминирован для тех же канонических полей (нормализация login: lowercase + trim).
  4. Credentials с одинаковым fingerprint объединяются в SupplierCredentialGroup (auto-merge только если выполнены все условия — см. credentials-and-tenancy.md policies).
  5. Privacy (Pricing inv): observation от credential client’а A с scope=customer не используется для client’а B (если B не member той же группы). Проверяется на уровне репозитория. 5a. master_key_token_exchange: plaintext access_token никогда не персистится в plain-text event store / логи. Кешируется в Redis в encrypted form с valid_until + автоматический refresh. MasterKey зашифрован через KMS, как и остальные secrets (ADR-0018). 5b. public_no_auth: credential не требует шифрования секретов (их нет), но все остальные инварианты (fingerprint, group, rate limit) сохраняются — нужны для tracking endpoint liveness.
  6. Любая DecryptForUse логируется в credential_decrypt_audit с обязательным полем reason (pricing_pipeline | manual_break_glass | rewrap_job | validation).
  7. CredentialBreakGlassPerformed требует двух approver’ов с ролью security (4-eyes). Это НЕ автоматизируется AI-агентом (см. Moderation BC invariant #4: security-critical всегда human). AI может только подсветить аномалию / записать risk score, но решение — за двумя людьми.
  8. Customer никогда не видит секретные поля после ввода — только метаданные.
  9. Удаление аккаунта customer’а → немедленная ротация DEK + 30 дней grace до физического удаления старого DEK.
  10. CredentialFeatures.media_unwatermarked активируется только по подписанному запросу поставщика (policy: MediaRightsGranted event требует source=supplier_confirmation, не customer-self-service). Offers BC использует этот флаг при parse media — без него все URLs сохраняются как watermarked=true.
  11. CredentialConstraints.egress_policy.fixed_ip_required = true ⇒ deploy системы отказывается поднимать ingestor без привязанного egress IP (ADR-0024).
  12. ClientSkuMap хранит только уникальные (customer_ref, supplier_ref, client_sku) → supplier_sku. Повторные импорты перезаписывают с audit-записью.

Резервный UX (customer credential недоступна)

Если pricing для customer’а вынужденно использует системную credential (его собственная failing / expired / revoked):

  • В response pricing.observation_source.fallback_reason = customer_credential_failing | customer_credential_expired | customer_credential_revoked | no_customer_credential.
  • В клиентском UI: badge «приближённая цена» + tooltip + CTA «Обновить учётные данные».
  • При первом fallback за сутки — email/notification клиенту.
  • Метрика pricing_fallback_to_system_total{customer_id, reason}.

Lifecycle

[draft] → [validating] → [active] → [failing] → [revoked]
                                  ↘ [expired] ↗

Интеграционные события (публикуем)

Топик: credentials.events.v1. Partition key: (supplier_id, credential_id).

ИмяКогда
SupplierCredentialActivated, SupplierCredentialFailing, SupplierCredentialRevoked, SupplierCredentialExpiredLifecycle
SupplierCredentialGroupMerged, SupplierCredentialGroupCreatedГруппы
CredentialBreakGlassPerformedДля security audit dashboard
DekRotated, MasterKeyRotatedДля observability

Подписанные интеграционные события

ИсточникСобытиеРеакция
CustomerCustomerRegisteredПодготовить per-customer DEK
CustomerPersonalDataDeletedНемедленная ротация DEK + физическое удаление через 30 дней
IngestionSessionAuthFailed (мульти)Триггер MarkFailing

Связи в context map

BCПаттернНазначение
Customerupstream (Customer → Credentials)Customer владеет credentials
IngestionOHS (Credentials → Ingestion)CredentialContext — стабильный API
PricingOHS (Credentials → Pricing)CredentialContext для observation routing
External (KMS / Vault)ACLMasterKeyProvider port изолирует

Мини event storming

flowchart LR
    U["🟫 Customer"]
    subgraph CR["Credentials"]
        ADD["🟦 AddCredential"]
        ECP["🟧 SupplierCredentialProvided"]
        ENC["🟪 Encrypt at rest"]
        EE["🟧 CredentialEncryptedAtRest"]
        VAL["🟦 ValidateCredential"]
        EV["🟧 SupplierCredentialValidated"]
        FP["🟦 ComputeFingerprint"]
        EM["🟧 SupplierCredentialGroupMerged"]
        ACT["🟧 SupplierCredentialActivated"]
        SC["🟨 SupplierCredential"]
        SG["🟨 SupplierCredentialGroup"]
    end
    subgraph KMS["🌐 KMS / Vault"]
        K["MasterKey"]
    end
    subgraph ING["Ingestion"]
        DISCOVER["🟪 Discovery job для новой credential"]
    end
    subgraph PR["Pricing"]
        INV["🟪 Invalidate cache"]
    end

    U --> ADD --> ECP --> ENC
    ENC --> K
    K --> EE
    EE --> VAL --> EV --> FP
    FP --> EM --> ACT
    ACT -.PL.-> DISCOVER
    ACT -.PL.-> INV

Связанные файлы