Добавление клиентских credentials — runbook

Пошаговая инструкция добавления учётной записи клиента у поставщика.

Предварительные требования

  • У клиента есть валидная учётная запись у поставщика (соответствующая auth_schema поставщика — login/pass, API key, OAuth, etc.).
  • У клиента согласовано с поставщиком использование API.
  • Клиент аутентифицирован в нашем личном кабинете.

Auth-схемы (HEAD 5e4ce54, 2026-05-06)

Поставщик объявляет auth_scheme в backend/internal/core/cabinet/credentials/supplier_catalog/catalog.go (Go-const, не runtime config). Реально используемый набор:

auth_schemeПоляПоставщики
basiclogin + passwordETM, IEK, Russvet
api_tokenapi_tokenSysteme Electric
master_keymaster_keyDKC

Расширения oauth2 / bearer_token / custom остаются target — см. ADR-0010 (Pluggable credential schemas). Сейчас пять поставщиков покрывают первые три auth-схемы. Добавление новой схемы требует:

  1. Новый AuthScheme enum в supplier_catalog/catalog.go.
  2. Frontend ветка AuthFieldsBySchema.tsx под новую схему.
  3. Connector соответствующего поставщика парсит JSON-payload секрета.

UI клиентского кабинета рендерится автоматически по secret_fields из SupplierMeta (catalog endpoint GET /api/cabinet/credentials/suppliers/catalog).

Сценарий A: клиент добавляет сам через UI

Live на 2026-05-06 — Cabinet Credentials cycle закрыт (tracium_cabinet_credentials_execution.md). UI на https://tracium.dev/cabinet/credentials. AddCredentialModal — 3-step wizard.

Шаг 1. Запуск

Клиент → Личный кабинет → Учётные записи поставщиков → «Добавить» → выбор поставщика из catalog’а.

Шаг 2. Выбор схемы

Если у поставщика несколько поддерживаемых схем — клиент выбирает (или подставляется default).

Шаг 3. Инструкция

Система показывает supplier-specific инструкцию:

  • Как получить нужные секреты у поставщика.
  • Какие договорные действия требуются с их стороны.
  • Какие данные мы будем запрашивать и как часто.

Шаг 4. Ввод данных

Клиент вводит секреты в форму, сгенерированную по auth schema. Поля с secret: true помечены и скрыты в UI. Форма отправляет данные напрямую в backend → шифрование → запись в БД.

Шаг 5. Согласие

Клиент подтверждает условия обработки.

Шаг 6. Fingerprint и проверка дедупликации

  • Backend вычисляет fingerprint по auth_payload (детерминированный, на основе auth_schema):

    fingerprint = sha256_hex(
        supplier_id || ':' ||
        auth_schema || ':' ||
        canonical_identity_field(auth_schema, auth_payload)
    )
    

    где canonical_identity_field:

    auth_schemaполе для hash
    login_passwordlowercase(trim(login))
    api_keysha256(api_key) (мы НЕ храним сам ключ в hash)
    oauth2provider_user_id (получается при первом успешном validation request)
    bearer_tokensha256(bearer_token)
    customопределяется в spec.md коннектора, обязательно reproducible

    Реализация — platform/credentials/fingerprint.go; референс-вектора покрыты unit-тестами на каждый supplier’а.

  • Если fingerprint совпадает с существующей active credential другого customer’а — авто-merge в SupplierCredentialGroup (по правилам в 10-business/contexts/credentials.md §“Auto-merge правила”).

  • Если совпадение только частичное (тот же login, разный пароль; тот же key_id, другой key) — НЕ merge; запись попадает в moderation_queue для оператора каталога.

  • Клиент об этом не информируется (это административная деталь).

Шаг 7. Валидация

  • Создаётся запись SupplierCredential со статусом validating.
  • Коннектор делает тестовый запрос (например, ETM: login + info request).
  • Успех → статус active, last_validated_at = now().
  • Ошибка → статус failing, ошибка показывается клиенту.

Шаг 8. Старт работы

  • Credential подхватывается ingestion’ом со следующего scheduled tick’а.
  • Клиенту показывается статус и метрика “первый успешный fetch”.

Сценарий B: ассистированное добавление через поддержку

То же, но шаги 1-5 выполняются поддержкой по запросу клиента. Все остальные шаги идентичны.

Для аудита: каждое такое добавление имеет created_by = support-agent-id.

Сценарий C: API для клиентов (Phase 6+)

Программное добавление через наш API:

POST /v1/credentials
{
  "supplier_id": "etm",
  "label": "Основной договор",
  "auth_schema": "login_password",
  "auth_payload": {
    "login": "...",
    "password": "..."
  },
  "features": ["catalog", "prices", "stock"]
}

Все требования сценария A применимы.

Процедура валидации

Каждая credential проходит валидацию:

  1. Сразу при создании — синхронно.
  2. По расписанию — раз в сутки.
  3. При каждом N-том запросе — sampling.

Валидация выполняется через Connector.Validate(ctx, cred).

Обработка ошибок

Если credential начинает отвечать ошибками auth:

  1. Помечаем failing после 3 подряд неудач.
  2. Бакет приостанавливается.
  3. Клиенту → уведомление.
  4. В UI кабинета → красный badge.
  5. Pricing для этого клиента временно работает на системной credential с пометкой.
  6. После повторной успешной валидации → возврат в active.

Idle credential: если не используется N дней — статус idle, на UI клиента — предложение проверить или удалить.

Отзыв

Клиент отзывает credential

  1. Кабинет → Credentials → “Отозвать”.
  2. Подтверждение.
  3. Событие SupplierCredentialRevoked.
  4. Если credential — единственный участник группы → группа удаляется. Иначе — primary переключается.
  5. Бакет удаляется.
  6. История observations не удаляется (для аудита), но помечается from_revoked_credential и не используется в новых pricing-запросах.

Удаление аккаунта клиента

Каскадно отзывает все его credentials. Observations анонимизируются (customer_ref → null) или архивируются согласно политике хранения.

Безопасность

См. ADR-0010 (envelope encryption), ADR-0018 (master key + DEK rotation), 10-business/contexts/credentials.md §“Безопасность”.

Bootstrap master-ключа (для оператора по env)

EnvЧто задать
prod / staging (Vault Transit)MASTER_KEY_PROVIDER=vault, VAULT_ADDR, VAULT_TOKEN (через k8s secret), VAULT_TRANSIT_KEY=tracium-master-v1
prod / staging (KMS)MASTER_KEY_PROVIDER=kms, KMS_KEY_ID=arn:... (или эквивалент yandex-cloud), AWS_REGION/YC_KEY_FOLDER
dev (sealed-file)MASTER_KEY_PROVIDER=sealed_file, MASTER_KEY_FILE=/etc/tracium/master.key.sealed, MASTER_KEY_PASSPHRASE (env)
single-dev localMASTER_KEY_PROVIDER=plaintext_env, MASTER_KEY_RAW=base64-32bytes, обязательно TRACIUM_ALLOW_INSECURE_MASTER_KEY=1 (без него backend паникует на старте); запрещено в любом deployment кроме одного локального разработчика

Процедура bootstrap:

  1. (prod/staging) Создать transit key в Vault: vault write -f transit/keys/tracium-master-v1. Backend получает токен с capabilities encrypt, decrypt, rewrap на этот ключ.
  2. Запустить backend → проверить /health — должно показать master_key.provider=vault, key_id=tracium-master-v1.
  3. Создать первый системный credential через admin UI; убедиться, что в credential_dek появилась запись с master_key_id=tracium-master-v1.

События аудита

Каждое из событий пишется в event_store (event-sourced) и в credential_decrypt_audit для расшифровок:

  • SupplierCredentialCreated{credential_id, customer_id, supplier_id, scope, created_by}
  • SupplierCredentialValidated{credential_id, outcome, validated_by}
  • SupplierCredentialMarkedFailing{credential_id, reason}
  • SupplierCredentialRevoked{credential_id, revoked_by, reason}
  • SupplierCredentialGroupMerged{group_id, members[]}
  • CredentialDekRotated{customer_id, reason, old_dek_id, new_dek_id}
  • CredentialDecrypted (только в credential_decrypt_audit, не в event_store): actor, credential_id, customer_id, master_key_id, reason, correlation_id

Break-glass

Расшифровка credential вне обычного pricing pipeline (для расследования инцидента / поддержки):

  1. Оператор открывает admin UI → Credentials → выбирает credential → “Request decryption for investigation”.
  2. Указывает reason (free text) и incident_ref (Linear / Jira тикет — обязательно).
  3. Запрос попадает в очередь approval — требуется второй approver с ролью security (принцип 4 глаз).
  4. После двух approve → расшифровка выполняется, plaintext отображается в UI на одну загрузку страницы (не сохраняется в browser storage), пишется событие CredentialDecrypted{reason="manual_break_glass", ...}.
  5. Автоматическая нотификация в security-канал (Slack / Telegram).

При попытке decrypt без второго approver’а — операция блокируется на уровне UI и API.

Транспорт

  • Все клиентские формы передают секреты только по HTTPS (TLS 1.2+; TLS 1.3 в prod).
  • CSP-политика admin/customer UI: default-src 'self'; form-action 'self'.
  • В публичном API — обязательная валидация content-type, размер payload ≤ 64 KB на endpoint POST /v1/credentials.

Метрики

Для каждой credential:

  • Успешных запросов / ошибок за период.
  • Использование rate budget.
  • Дата последней валидации.
  • Покрытие observations.

Открытые вопросы

  • Какой backoff и период повторной валидации использовать после серии ошибок логина.
  • Кто подтверждает автоматическое объединение одинаковых credentials в одну группу, если fingerprint совпал.
  • Какой UX уведомлений считаем обязательным, если pricing временно перешёл на системную credential.