Контекст: учётные записи
NOTE
Статус: Target design. Документ описывает целевую доменную модель. Соответствующий код реализован частично (см.
backend/internal/core/) или пока не начат. Правила маркировки — в50-processes/documentation-standard.md.
Назначение
Proposal pipeline (
search-proposal.md, Layer 1.2) используетCredentialRouter.Select. Precedencecustomer → systemразрешается один раз per briefing с учётомCredentialConstraints(manufacturer_scope/classification_tag_scope/warehouse_scope). После выбора все чтения идут по exactcredential_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. |
CredentialScope | VO enum | system / customer. |
AuthSchema | VO enum | login_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 (КЭАЗ). |
TokenExchangePayload | VO | Для master_key_token_exchange: {master_key (encrypted), last_token?, last_token_valid_until?, exchange_endpoint}. Token refresh — автоматический через SessionManager. |
AuthPayload | VO | JSONB; secret: true поля шифрованы. |
AuthMetadata | VO | Открытые поля. |
Fingerprint | VO | sha256(supplier_id + auth_schema + canonical_identity_field(payload)). |
SupplierCredentialGroup | E | Группа credentials с одинаковым fingerprint. |
CredentialFeatures | VO enum-set | catalog / prices / stock / place_orders / media_unwatermarked / dictionary_exports / async_reports. |
CredentialConstraints | VO | (warehouse_scope?, classification_tag_scope?, manufacturer_scope?, egress_policy?) — ограничения, выставленные поставщиком на данную credential. |
EgressPolicy | VO | (fixed_ip_required, allowed_cidrs?) — для поставщиков, требующих whitelisted source IP (например, ETM для foreign hosting). |
CredentialStatus | VO enum | draft / validating / active / failing / expired / revoked. |
CredentialContext | VO | Shared Kernel — передаётся в Connector. Несёт features, constraints, ключи AuthBucket и DataBucket для rate limiter. |
MasterKeyProvider | ⚙️ Port | Vault Transit / KMS / sealed-file. |
DEK | VO | Per-customer Data Encryption Key (зашифрован master key). |
RateLimitOverrides | VO | Per-credential overrides по {auth: {...}, data: {endpoint: {...}}}. |
WarehouseScope | VO | Список доступных складов (подмножество графа Supplier Network). |
ClientSkuMap | 🟨 Aggregate | Cross-reference клиентских SKU на коды поставщика. Owner: Credentials BC (тесно связан с credential). Идентификация: (customer_ref, supplier_ref, credential_ref). |
ClientSkuMapping | VO | (client_sku, supplier_sku, manufacturer_article?, verified_at, source). |
ClientSkuMappingSource | VO enum | manual_csv / dictionary_export / observed_in_payload / operator. |
CredentialDecryptAuditEntry | E | append-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 / Operator | SupplierCredential | SupplierCredentialProvided + CredentialEncryptedAtRest |
ПроверитьУчётнуюЗапись (ValidateCredential) | Worker | SupplierCredential | SupplierCredentialValidated или SupplierCredentialFailing |
АктивироватьУчётнуюЗапись (ActivateCredential) | Policy | SupplierCredential | SupplierCredentialActivated |
ОтметитьСбой (MarkFailing) | Connector | SupplierCredential | SupplierCredentialFailing |
ОтозватьУчётнуюЗапись (Revoke) | Customer / Operator | SupplierCredential | SupplierCredentialRevoked |
ВычислитьFingerprint (ComputeFingerprint) | Engine | — | — |
ПрисоединитьКГруппе (MergeIntoGroup) | Policy | SupplierCredentialGroup | SupplierCredentialGroupMerged |
РасшифроватьДляИспользования (DecryptForUse) | Connector / Pricing | — | CredentialDecrypted (audit) |
BreakGlassРасшифровка (BreakGlassDecrypt) | Security (4-eyes) | — | CredentialBreakGlassPerformed |
ПовернутьDEK (RotateDek) | Schedule / Incident | — | DekRotated |
Политики
| Триггер | Реакция |
|---|---|
SupplierCredentialProvided | → EncryptAtRest (DEK через master key) |
CredentialEncryptedAtRest | → ValidateCredential (тестовый запрос согласно provider contract tests ADR-0023) |
SupplierCredentialValidated | → ComputeFingerprint, поиск группы |
Fingerprint match + все условия auto-merge | → MergeIntoGroup |
Fingerprint partial match | → publish CredentialGroupCandidateOpened → AI-агент credential_group_candidate в Moderation BC (видит метаданные, никогда секреты) |
ProposedAction(MergeCredentialGroup) от Moderation | → MergeIntoGroup + ack DecisionApplied |
SessionExpired (Ingestion) | → не инкрементирует счётчик failing. SessionManager запускает re-login. |
SessionAuthFailed (AuthRejected после свежего login) 3+ за 10 мин | → MarkFailing |
SupplierCredentialFailing | → notification клиенту, badge в кабинете |
| Customer renewed credential | → ValidateCredential снова |
CustomerCredentialActivated + connector имеет dictionary_exports capability | → EnqueueEnrichmentJob{kind=client_sku_map_import} для пополнения ClientSkuMap |
| Schedule (180 дней) | → RotateDek per-customer |
| Compromise master key | → RotateDek для всех (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.
Инварианты
scope = customer⇒customer_refне пуст.- Сами секреты (
secret: true) хранятся только в зашифрованном виде. Plaintext не попадает в логи / PG snapshots / errors. fingerprintдетерминирован для тех же канонических полей (нормализация login: lowercase + trim).- Credentials с одинаковым fingerprint объединяются в
SupplierCredentialGroup(auto-merge только если выполнены все условия — см.credentials-and-tenancy.mdpolicies). - Privacy (Pricing inv): observation от credential client’а A с scope=customer не используется для client’а B (если B не member той же группы). Проверяется на уровне репозитория.
5a.
master_key_token_exchange: plaintextaccess_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. - Любая
DecryptForUseлогируется вcredential_decrypt_auditс обязательным полемreason(pricing_pipeline | manual_break_glass | rewrap_job | validation). CredentialBreakGlassPerformedтребует двух approver’ов с рольюsecurity(4-eyes). Это НЕ автоматизируется AI-агентом (см. Moderation BC invariant #4: security-critical всегда human). AI может только подсветить аномалию / записать risk score, но решение — за двумя людьми.- Customer никогда не видит секретные поля после ввода — только метаданные.
- Удаление аккаунта customer’а → немедленная ротация DEK + 30 дней grace до физического удаления старого DEK.
CredentialFeatures.media_unwatermarkedактивируется только по подписанному запросу поставщика (policy:MediaRightsGrantedevent требуетsource=supplier_confirmation, не customer-self-service). Offers BC использует этот флаг при parse media — без него все URLs сохраняются какwatermarked=true.CredentialConstraints.egress_policy.fixed_ip_required = true⇒ deploy системы отказывается поднимать ingestor без привязанного egress IP (ADR-0024).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, SupplierCredentialExpired | Lifecycle |
SupplierCredentialGroupMerged, SupplierCredentialGroupCreated | Группы |
CredentialBreakGlassPerformed | Для security audit dashboard |
DekRotated, MasterKeyRotated | Для observability |
Подписанные интеграционные события
| Источник | Событие | Реакция |
|---|---|---|
| Customer | CustomerRegistered | Подготовить per-customer DEK |
| Customer | PersonalDataDeleted | Немедленная ротация DEK + физическое удаление через 30 дней |
| Ingestion | SessionAuthFailed (мульти) | Триггер MarkFailing |
Связи в context map
| BC | Паттерн | Назначение |
|---|---|---|
| Customer | upstream (Customer → Credentials) | Customer владеет credentials |
| Ingestion | OHS (Credentials → Ingestion) | CredentialContext — стабильный API |
| Pricing | OHS (Credentials → Pricing) | CredentialContext для observation routing |
| External (KMS / Vault) | ACL | MasterKeyProvider 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
Связанные файлы
- Сценарий:
../scenarios/credential-onboarding.md,../scenarios/customer-auth.md,../scenarios/supplier-dictionary-sync.md. customer.md,ingestion.md,pricing.md.- ADR-0010 — pluggable credential schemas + envelope encryption.
- ADR-0018 — master key и DEK rotation policy.
- ADR-0011 — no-proxy.
- ADR-0024 — supplier connector contract (CredentialFeatures расширен; EgressPolicy требования; error taxonomy
SessionExpiredvsAuthRejected).