Перейти к содержанию

Cookbook: Добавить новый service-key

Как добавить новый HMAC-ключ внешнего сервиса в PM с ограниченным набором операций.

Цель

Добавить новый serviceId в таблицу pm.service_key, чтобы внешний сервис мог вызывать PM-endpoints (POST /intents, /intents/quote, …) через HMAC-подпись. Ключ должен иметь минимально достаточные права: список allowedOperationTypes и (опционально) маски от/до-счетов.

Пример в этом гайде — auth-center-merchant: ключ Auth Center'а, которому разрешён только NFC_CHARGE (списание с user/agent wallet в merchant wallet).

Предусловия

  • Локальный PM запущен, есть доступ к pm.* в Postgres.
  • Понимаешь, какие operationType нужны ключу — см. ../reference/passport/03-operation-types.md.
  • Понимаешь, какие AccountType участвуют — см. ../reference/database/06-tb-account-map.md.
  • У сервиса-клиента есть возможность хранить HMAC-секрет в своём env (PM сам секреты в БД не хранит).

Как это работает

PM проверяет HMAC-подпись каждого входящего запроса в плагине src/auth/hmacPlugin.ts:

  1. Из заголовка X-Service-Id берётся serviceId.
  2. Секрет для этого serviceId ищется в env-переменной SERVICE_SECRETS (одна общая JSON-карта { "<service-id>": "<base64-secret>", … }).
  3. Проверяется подпись (src/auth/hmac.ts:verifyHmacRequest) — HMAC-SHA256(secret, "ts\nMETHOD\npath\nsha256(body)"), timestamp-drift ≤ 60 секунд.
  4. После успешной проверки HMAC из pm.service_key подгружаются permissions для этого serviceId (active=true). Дальше PM по этим permissions решает, можно ли выполнять запрошенный operationType и какие счета допустимы.

Ключевая идея: секрет — в env, права — в БД. Поэтому добавление нового ключа — это всегда две правки: env (для подписи) и миграция (для прав).

Шаги

1. Сгенерировать секрет

openssl rand -base64 48

Сохрани значение — оно понадобится и PM, и сервису-клиенту. Минимум 32 байта энтропии; чаще используем 48 байт base64.

2. Добавить секрет в SERVICE_SECRETS

В .env PM (и в env сервиса-клиента, если он сам подписывает запросы) SERVICE_SECRETS — это один JSON-объект со всеми ключами:

# .env (PM)
SERVICE_SECRETS='{
  "auth-center":          "…",
  "nginx-gateway":        "…",
  "admin-tool":           "…",
  "admin-panel":          "…",
  "auth-center-merchant": "<новый base64-секрет>"
}'

Никаких SERVICE_KEY_SECRET_AUTH_CENTER или per-service env-переменных — структура одна, см. src/shared/config.ts (envSchema.SERVICE_SECRETS парсится через JSON.parse).

В drizzle/seed.ts есть deploy-time guard: если в SERVICE_SECRETS не оказалось одного из обязательных ключей — seed упадёт. При добавлении нового обязательного ключа обнови этот список (см. шаг 4).

3. Создать миграцию drizzle/migrations/NNNN_<name>_service_key.sql

Следующий свободный номер — 0010_…, 0011_… и т.д. (посмотри drizzle/migrations/). Имя — kebab-case, короткое и по делу: 0010_add_auth_center_merchant_service_key.sql.

-- 0010_add_auth_center_merchant_service_key.sql
--
-- auth-center-merchant: NFC_CHARGE only.
-- Списание с USER_WALLET / AGENT_WALLET в MERCHANT_WALLET через INTERNAL_P2P.
-- Секрет НЕ хранится в БД — только в env SERVICE_SECRETS.

INSERT INTO pm.service_key (service_id, permissions, active)
VALUES (
  'auth-center-merchant',
  '{
    "allowedOperationTypes": ["NFC_CHARGE"],
    "allowToTbAccountId": false,
    "fromAccountOverride": {
      "allowedTypes": ["USER_WALLET", "AGENT_WALLET"],
      "namePattern":  "^(user\\.|agent\\.)"
    },
    "toAccountOverride": {
      "allowedTypes": ["MERCHANT_WALLET"],
      "namePattern":  "^merchant\\."
    }
  }'::jsonb,
  true
)
ON CONFLICT (service_id) DO UPDATE SET
  permissions = EXCLUDED.permissions,
  active      = true;

Поля permissions (см. src/shared/schema.ts:ServicePermissions):

Поле Назначение
allowedOperationTypes Белый список operationType для этого ключа. Пусто = ничего нельзя инициировать (только read-only).
merchantId Жёстко привязывает ключ к одному merchantId (используется, например, для интеграций конкретного мерчанта).
forceResolve Разрешает PM авто-резолвить from/to по userId, минуя явное указание счёта. Опасно — давай только админ-ключам.
fromAccountOverride / toAccountOverride Маски на AccountType и имя счёта. Первая линия защиты — allowedTypes, namePattern лишь страхует форму имени.
allowToTbAccountId Разрешает использовать сырой TB account id в toAccount (обычно нужно auth-center, чтобы платить по system.* счетам).

Чтобы по-русски подсветить зачем именно так — оставь короткие комментарии прямо в SQL (см. seed.ts:auth-center-merchant, namePattern — это вторичный guard для shape-имени, основная защита — allowedTypes).

Канал — ADMIN (для админских корректировок) или INTERNAL_P2P (для пуш-charge), но не ADMIN_TRANSFER: это operationType, а не канал. Каналы — в ../reference/passport/02-channels.md.

4. Обновить drizzle/seed.ts (если ключ "обязательный")

Если новый ключ должен присутствовать в любой dev/staging/prod-инсталляции — добавь его и в seed.ts, чтобы локальный npm run db:seed создавал ту же запись и чтобы guard в начале seed'а отлавливал отсутствие секрета:

// seed.ts — фрагмент seeds[]
{
  serviceId:   'auth-center-merchant',
  permissions: {
    allowedOperationTypes: ['NFC_CHARGE'],
    allowToTbAccountId:    false,
    fromAccountOverride: {
      allowedTypes: ['USER_WALLET', 'AGENT_WALLET'],
      namePattern:  '^(user\\.|agent\\.)',
    },
    toAccountOverride: {
      allowedTypes: ['MERCHANT_WALLET'],
      namePattern:  '^merchant\\.',
    },
  },
  active: true,
},

И в guard'е if (!secrets['auth-center'] || …) — добавь проверку !secrets['auth-center-merchant'].

Если ключ нужен только в одной среде (например, временная интеграция) — достаточно миграции, в seed.ts его добавлять не нужно. Аналогично exchange-webhook в seed'е — он создаётся только если секрет реально присутствует в env.

5. Применить миграцию

cd projects/payment-manager
npx drizzle-kit migrate

Проверить запись:

SELECT service_id, permissions, active
FROM pm.service_key
WHERE service_id = 'auth-center-merchant';

6. Обновить документацию

  • ../reference/passport/06-service-keys.md — добавить строку про новый serviceId, его разрешения и канал использования.
  • ../reference/database/11-service-key.md — обновлять только если изменилась схема таблицы или структура ServicePermissions (новое поле и т.п.). Чисто новая строка-данные сюда не идёт.

7. Обновить CHANGELOG.md

В корне PM:

## [Unreleased]
### Added
- service_key: добавлен `auth-center-merchant` (NFC_CHARGE only, USER/AGENT → MERCHANT).

Проверка вручную (smoke-test)

  1. Сервис-клиент берёт секрет, генерирует подпись по signRequest(secret, ts, 'POST', '/intents/quote', sha256hex(body)) (см. src/auth/hmac.ts:signRequest).
  2. Шлёт POST /intents/quote с заголовками:
  3. X-Service-Id: auth-center-merchant
  4. X-Timestamp: <unix-sec>
  5. X-Signature: <hex>
  6. Ожидаемый ответ — 200 для operationType: 'NFC_CHARGE'; 403 (или INSUFFICIENT_PRIVILEGE) для любого другого operationType, например P2P_TRANSFER.
  7. Если from.account начинается с system.… — PM должен отказать (маска ^(user\.|agent\.) запрещает).

Откат

Чтобы временно вырубить ключ без миграции — обнови строку в БД:

UPDATE pm.service_key SET active = false WHERE service_id = 'auth-center-merchant';

Полное удаление — отдельной миграцией (DELETE FROM pm.service_key WHERE …), и только после ротации секрета в env у всех клиентов.

Связанные документы

  • ../reference/passport/06-service-keys.md — канонический список всех serviceId и их прав.
  • ../reference/database/11-service-key.md — схема таблицы pm.service_key.
  • ../reference/passport/02-channels.md — список каналов (INTERNAL_P2P, ADMIN, IPPS_TRANSFER, …).
  • ../reference/passport/03-operation-types.md — список operationType.
  • src/auth/hmac.ts, src/auth/hmacPlugin.ts — реализация HMAC-проверки.
  • src/shared/config.ts — env-схема (SERVICE_SECRETS).
  • drizzle/seed.ts — образцы seed-конфигов ключей.