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

Секреты интеграций — хранение и чтение

1. Назначение

Модуль Integration Secrets обеспечивает централизованное хранение секретов внешних сервисов (логины, пароли, API-ключи, токены). Секрет идентифицируется по строковому ключу serviceKey и содержит произвольный payload — набор пар «поле → значение».

Все серверные компоненты читают секреты через единый read-only фасад ISecretsProvider. Прямая работа с таблицами *Credentials / *Settings в новом коде не допускается.

Структура сущности

Каждый секрет содержит:

  • serviceKey — уникальный строковый ключ-идентификатор секрета.
  • displayName — отображаемое имя (обязательно, ограничено по длине).
  • payload — набор пар «поле → значение» (Login, Password, ApiKey, AccessToken и др.). Значения хранятся в зашифрованном виде.

2. Ключевые файлы

Файл Назначение
Valhalla.Interfaces/Secrets/ISecretsProvider.cs Контракт провайдера
Valhalla.Secrets/Provider/CompositeSecretsProvider.cs Реализация: new → legacy fallback
Valhalla.Secrets/Provider/LegacySecretsAdapter.cs Адаптер legacy-таблиц
Valhalla.Integration/Valhalla.Integration/Contracts/Secrets/SecretServiceKeys.cs Константы префиксов serviceKey
Valhalla.ExternalServices/Configuration/*ExternalServicesConfigurationService.cs Потребители ISecretsProvider

3. Custom setting UseIntegrationSecrets

Читается в CompositeSecretsProvider как feature flag.

Значение Поведение
true Сначала ищет в IntegrationSecrets; при отсутствии записи — fallback в legacy (*Credentials / *Settings)
false (по умолчанию) Всегда идёт только в legacy

Настраивается через Admin UI → Настройки системы → Custom settings, ключ UseIntegrationSecrets.


4. Архитектура провайдера

ISecretsProvider
    └── CompositeSecretsProvider
            ├── IIntegrationSecretsStore   (новое хранилище, зашифрованный payload)
            └── LegacySecretsAdapter       (legacy *Credentials / *Settings)

LegacySecretsAdapter парсит serviceKey как {prefix}-{serviceId}, резолвит нужный ILegacySecretResolver через DI и возвращает payload из legacy-таблицы. Аудит при legacy-чтении не пишется.


5. Сервисы с готовой поддержкой ISecretsProvider

Все 14 сервисов уже инжектируют ISecretsProvider. При включении UseIntegrationSecrets = true они автоматически начнут читать из нового хранилища — изменений кода не требуется.

Префикс serviceKey Configuration-сервис Legacy-резолвер
ldap ActiveDirectoryExternalServicesConfigurationService, SynchronizationProfilesService ActiveDirectoryLegacySecretResolver
openldap OpenLDAPExternalServicesConfigurationService OpenLDAPLegacySecretResolver
radius RadiusExternalServicesConfigurationService RadiusLegacySecretResolver
diadoc DiadocExternalServicesConfigurationService DiadocLegacySecretResolver
sbis SBISExternalServicesConfigurationService SbisLegacySecretResolver
ews EwsExternalServicesConfigurationService EwsLegacySecretResolver
oauth OAuthExternalServicesConfigurationService OAuthLegacySecretResolver
cryptopro-dss CryptoProExternalServicesConfigurationService CryptoProDssLegacySecretResolver
azure-cognitive AzureExternalServicesConfigurationService AzureCognitiveLegacySecretResolver
pt-sandbox PTSandboxExternalServicesConfigurationService, PTSandboxScanService PTSandboxLegacySecretResolver
kontur-cloud KonturCloudServicesConfigurationService KonturCloudLegacySecretResolver
kontur-kedo KonturKEDOServicesConfigurationService KonturKEDOLegacySecretResolver
multifactor MultifactorExternalServicesConfigurationService MultifactorLegacySecretResolver
jitsi JitsiExternalServicesConfigurationService JitsiLegacySecretResolver

5.1. Почтовые ящики (mailbox / servicemailbox)

В отличие от 14 интеграционных сервисов выше, для почтовых ящиков (mailbox-{MailBoxID} — пользовательские из EmailMailBoxes; servicemailbox-{Id} — сервисные из ServiceMailBoxes) перенос записи в IntegrationSecrets отложен на следующие итерации. На данный момент:

  • Чтение идёт через ISecretsProvider стандартным маршрутом: CompositeSecretsProviderIntegrationSecrets (если запись существует) → fallback в LegacySecretsAdapterMailBoxLegacySecretResolver / ServiceMailBoxLegacySecretResolver. Резолверы читают зашифрованный пароль из legacy-колонок и расшифровывают через IMailBoxPasswordCipher (MailBoxPasswordCipher.cs): значения с префиксом VENC: — через EncryptionService.Decrypt, остальные — через старый SimpleAES.Decrypt ради обратной совместимости.
  • Запись ведётся в те же legacy-колонки EmailMailBoxes / ServiceMailBoxes, но шифруется уже через EncryptionService (MailBoxPasswordCipher.EncryptForStorage). Установка UseIntegrationSecrets = true для почтовых ящиков пока ничего не меняет — пока никто не запишет payload в IntegrationSecrets (через Admin API или скрипт), резолвер вернёт значение из legacy-колонки.
  • Контракты payload (для будущего переноса записи в IntegrationSecrets): MailBoxSecretPayload { Password, CalDavPassword }, ServiceMailBoxSecretPayload { Password } — см. § 8.
  • Массовая перешифровка legacy-значений (SimpleAESEncryptionService) — Lua-утилита MailBoxPasswordReEncryptionService:ReEncryptAll() (запускать после раскатки кода на все инстансы). Файл: TCClassLib/Services/Mail/MailBoxPasswordReEncryptionService.cs. Подробности — в mail/admin.md § Хранение паролей ящиков.

6. Миграция конкретного сервиса

Кодовых изменений не требуется. Достаточно:

  1. Включить UseIntegrationSecrets = true в custom settings (однократно для всего экземпляра).
  2. Создать запись в IntegrationSecrets с нужным serviceKey через Admin API или смарт-скрипт:
UTILS.set_secret_payload("diadoc-1", { Login: "user@domain.ru", Password: "secret" });

После этого CompositeSecretsProvider начнёт возвращать значения из нового хранилища. Legacy-запись продолжает работать как fallback для остальных сервисов, у которых записи ещё нет.


7. Admin API

Endpoint: api/admin/secrets (только god-admin).

Метод URL Назначение
GET api/admin/secrets Список секретов (без значений payload)
GET api/admin/secrets/{serviceKey} Карточка секрета (только payloadKeys)
GET api/admin/secrets/{serviceKey}/payload Расшифрованный payload (фиксируется в аудите)
PUT api/admin/secrets/{serviceKey} Создание / обновление
DELETE api/admin/secrets/{serviceKey} Деактивация (не физическое удаление)
GET api/admin/secrets/contracts Список зарегистрированных контрактов payload
GET api/admin/secrets/{serviceKey}/contract Контракт для конкретного ключа
POST api/admin/secrets/{serviceKey}/audit Аудит по секрету
POST api/admin/secrets/audit Общий аудит с фильтрацией
POST api/admin/secrets/{serviceKey}/migrate-to-external Перенос локального секрета в alias на внешнее хранилище (body MigrateToExternalRequest: ExternalServiceId, ExternalPath). Создаёт item во внешнем хранилище (Passwork), обнуляет PayloadEncrypted в БД. Аудит: MigratedToExternal
POST api/admin/secrets/{serviceKey}/migrate-to-local Обратная миграция: payload вычитывается из внешнего хранилища, шифруется и сохраняется в БД 1F. Item во внешнем хранилище не удаляется (защита от потери данных — удаляет админ через UI хранилища). Аудит: MigratedToLocal
GET api/admin/secrets/external/storages Список инстансов внешних хранилищ (для UI «куда мигрировать»). Берётся из ServicesSettings по ExternalServiceType in { Passwork }
POST api/admin/secrets/external/{serviceId:int}/passwork/refresh-tokens Ручной refresh access/refresh-токенов Passwork-инстанса (см. passwork.md § 4) — обновляет PassworkCredentials.AccessToken/RefreshToken
POST api/admin/secrets/external/{serviceId:int}/verify Probe-запрос к внешнему хранилищу: статус Ok / Warning / Critical (PassworkInstanceStatus). Результат сохраняется в PassworkValidator.LastResult
POST api/admin/secrets/legacy/migrate-all Массовая миграция legacy *Credentials-таблиц в единое IntegrationSecrets. Возвращает LegacySecretsMigrationResultDto (счётчики по сервисам)
POST api/admin/secrets/legacy/migrate Миграция одного legacy-сервиса (body — serviceKey либо serviceType + serviceId)

Разделение метаданных и значений. Списковые и карточечные методы возвращают только payloadKeys — имена полей payload без их значений. Расшифрованные значения доступны исключительно через отдельный endpoint (/payload). Каждый вызов этого endpoint фиксируется в аудите.

Деактивация vs удаление. DELETE выполняет деактивацию записи, а не физическое удаление. Это сохраняет историю в журнале аудита и позволяет отменить операцию при необходимости.


8. Контракты payload

Для зарезервированных префиксов зарегистрированы контракты — допустимые поля payload. При создании/обновлении секрет валидируется против контракта. Список префиксов совпадает с таблицей в п. 5.

Если serviceKey не относится ни к одному зарезервированному префиксу, контракт отсутствует и валидация по полям не выполняется.


9. Валидация при создании и обновлении

При PUT api/admin/secrets/{serviceKey} платформа проверяет:

  • serviceKey — должен соответствовать допустимому формату ключа.
  • displayName — обязателен, ограничен по длине.
  • payload — должен быть непустым объектом.
  • Для зарезервированных префиксов дополнительно проверяется соответствие payload зарегистрированному контракту (допустимые поля).

10. Аудит

Каждое обращение к payload фиксируется с полями: serviceKey, тип действия, пользователь, момент, IP, источник (adminApi / smartScript). Legacy-чтение через LegacySecretsAdapter в аудит не попадает.

Значения IntegrationSecretAuditAction: Read, Created, Updated, Deleted, MigratedToExternal (payload перенесён во внешнее хранилище — alias, см. § 13), MigratedToLocal (обратная миграция alias → локальное хранение).


11. Смарт-скрипты

// JavaScript — чтение
var apiKey = UTILS.get_secret_value("sbis-1", "ApiKey");
var creds  = UTILS.get_secret_payload("sbis-1");

// JavaScript — запись
UTILS.set_secret_value("sbis-1", "ApiKey", newKey);
UTILS.set_secret_payload("sbis-1", { ApiKey: newKey, Login: "user" });
-- Lua — чтение
local apiKey = UTILS:get_secret_value("sbis-1", "ApiKey")
local creds  = UTILS:get_secret_payload("sbis-1")

-- Lua — запись
UTILS:set_secret_value("sbis-1", "ApiKey", newKey)
UTILS:set_secret_payload("sbis-1", { ApiKey = newKey, Login = "user" })

Если секрет или поле не найдены — возвращается null, исключение не бросается. Каждый вызов фиксируется в аудите с источником smartScript.


12. Admin UI (AdminSPA, v2.268+, задача #2056426)

Маршрут: /administration/secrets (god-admin). SPA-страница для CRUD над хранилищем IntegrationSecrets через api/admin/secrets (см. § 7).

Экраны:

Экран URL Назначение
Список секретов /administration/secrets DataSourceGrid с filtering. Кнопки «Создать», «Общий журнал аудита»
Форма секрета side-panel из списка Создание / редактирование
Журнал по секрету /administration/secrets/{serviceKey}/audit Аудит операций конкретного секрета
Общий журнал /administration/secrets/audit Аудит по всем секретам с фильтрацией

Колонки списка (Ag-Grid): ServiceKey · displayName · Status (чип: «Активен» / «Деактивирован») · Payload (chips, имена ключей) · Created · Updated.

Контекстное меню строки: Редактировать · Показать значения · Аудит · Деактивировать (см. § 7 — DELETE выполняет деактивацию, а не физическое удаление).

Форма создания / редактирования:

  • Service Key — обязательный идентификатор. Регэксп: [a-z0-9.-]+, ≥2 символов. При редактировании поле disabled (изменение ключа делает аудит бессмысленным; для смены — пересоздание).
  • Название (displayName) — опциональный, до 512 символов.
  • Payload — динамический массив пар «ключ → значение», ключ до 128 символов, значение до 8192. Структура payload произвольная: набор полей определяется тем, что вводит админ, а не зафиксированной схемой (контракт по § 8 применяется только к зарезервированным префиксам). Дубликат ключа — валидационная ошибка. Значения вводятся через input[type=password] с toggle-маской по иконке-глазу. Автозаполнение браузера отключено (autocomplete="new-password").
  • Можно добавлять/удалять пары через «Добавить поле».

Просмотр payload (readonly):

ReadOnly-таблица с колонками «Ключ», «Значение», «Действие». Значения по умолчанию замаскированы как ••••••••; кнопка-глаз раскрывает конкретное поле — каждый раскрытие фиксируется в аудите как Read через GET api/admin/secrets/{serviceKey}/payload.

Журнал аудита:

Фильтр Значение
Период datepicker (С / По) с точностью до минуты
Действие чипы: Created, Updated, Read, Deleted
Пользователь инициатор операции
Пагинация стандартная (skip/take)

Каждая строка журнала раскрывается до деталей операции (см. поля AuditEntry в § 7 и таблицу IntegrationSecretAudit).

Связанная фича: конфигурация самих сервисов интеграций (URL, порты, флаги) управляется через другую SPA-форму /administration/services — см. admin.md § AdminSPA — единая форма «Сервисы». ServiceType в Сервисах соответствует префиксу serviceKey в Секретах.


13. Внешнее хранилище секретов и alias-записи

С v2.268 запись в IntegrationSecrets может хранить payload не в самой 1F, а во внешнем хранилище (Passwork v7 — единственный реализованный провайдер; интерфейс IExternalSecretsClient рассчитан на расширение). Локальная строка в этом случае становится alias — указателем на item во внешнем сейфе. Детали по Passwork — в passwork.md.

13.1. Модель данных

Колонки, добавленные миграцией 1778500000.2056425-add-IntegrationSecrets-external-storage.sql к таблице IntegrationSecrets:

Колонка Тип Семантика
ExternalServiceId int NULL (FK → ServicesSettings(Id)) Инстанс внешнего хранилища, в котором лежит payload. NULL для локальных записей
ExternalSecretId nvarchar(256) NULL Идентификатор item-а во внешнем хранилище (для Passwork — itemId)
PayloadEncrypted varbinary(max) NULL Стало nullable. Для alias-записей всегда NULL — payload лежит снаружи

Инвариант: либо PayloadEncrypted IS NOT NULL и ExternalServiceId IS NULL (локальный секрет), либо наоборот (alias). Гибридного режима нет.

13.2. Поток «миграция в alias» (MigratedToExternal)

  1. Админ вызывает POST api/admin/secrets/{serviceKey}/migrate-to-external с body MigrateToExternalRequest { ExternalServiceId, ExternalPath }.
  2. IntegrationSecretsService дешифрует текущий PayloadEncrypted, маппит payload через PassworkItemMapper в DTO PassworkCreateItemRequest.
  3. PassworkClient.CreateItemAsync создаёт item в сейфе (DefaultVaultId или путь из ExternalPath, см. passwork.md § 7).
  4. После успешного ответа IntegrationSecrets обновляется атомарно: PayloadEncrypted = NULL, ExternalServiceId = ..., ExternalSecretId = ....
  5. Аудит: MigratedToExternal с ExternalServiceId/ExternalSecretId в payload-дельте.

Запрос отклоняется, если у Passwork-инстанса SupportsWrite = 0 либо PassworkValidator.LastResult = Critical.

13.3. Поток «обратная миграция» (MigratedToLocal)

  1. POST api/admin/secrets/{serviceKey}/migrate-to-local (без body).
  2. IntegrationSecretsService читает item из внешнего хранилища через PassworkExternalSecretsClient.GetSecretAsync.
  3. Payload шифруется штатным IEncryptionService и пишется в PayloadEncrypted.
  4. ExternalServiceId = NULL, ExternalSecretId = NULL.
  5. Item во внешнем хранилище не удаляется (защита от случайной потери — удаление остаётся ручной операцией в UI хранилища).
  6. Аудит: MigratedToLocal.

13.4. Чтение и запись alias-секретов в runtime

SecretsProvider при чтении проверяет ExternalServiceId: - NULLLocalSecretsProvider дешифрует PayloadEncrypted. - NOT NULLExternalSecretsProviderRouter берёт IExternalSecretsClient по ServiceType инстанса и вызывает GetSecretAsync(externalSecretId).

Запись по alias (UTILS.set_secret_value из смарт-скриптов и Admin UI) симметричная: IExternalSecretsClient.UpdateSecretAsyncPassworkClient.UpdateItemAsync. Локальный PayloadEncrypted остаётся NULL.

13.5. Verify / диагностика

POST api/admin/secrets/external/{serviceId}/verify запускает probe внешнего хранилища (PassworkValidator.RunProbe) и возвращает PassworkInstanceStatus. Используется UI «Сервисы» и SelfTest.

13.6. Legacy-миграция

Параллельно с external-storage в v2.268 завершается миграция от множественных таблиц *Credentials (по сервису) к единому IntegrationSecrets:

  • POST api/admin/secrets/legacy/migrate-all — пройти по всем зарегистрированным ILegacySecretSource и перенести payload в IntegrationSecrets (с шифрованием). Возвращает LegacySecretsMigrationResultDto со счётчиками по сервисам.
  • POST api/admin/secrets/legacy/migrate — то же для одного сервиса (serviceKey либо serviceType + serviceId).

После миграции LegacySecretsAdapter остаётся как fallback на чтение (на случай если кто-то ещё пишет в legacy-таблицу напрямую), но основным источником становится IntegrationSecrets.