Секреты интеграций — хранение и чтение¶
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стандартным маршрутом:CompositeSecretsProvider→IntegrationSecrets(если запись существует) → fallback вLegacySecretsAdapter→MailBoxLegacySecretResolver/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-значений (
SimpleAES→EncryptionService) — Lua-утилитаMailBoxPasswordReEncryptionService:ReEncryptAll()(запускать после раскатки кода на все инстансы). Файл:TCClassLib/Services/Mail/MailBoxPasswordReEncryptionService.cs. Подробности — в mail/admin.md § Хранение паролей ящиков.
6. Миграция конкретного сервиса¶
Кодовых изменений не требуется. Достаточно:
- Включить
UseIntegrationSecrets = trueв custom settings (однократно для всего экземпляра). - Создать запись в
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)¶
- Админ вызывает
POST api/admin/secrets/{serviceKey}/migrate-to-externalс bodyMigrateToExternalRequest { ExternalServiceId, ExternalPath }. IntegrationSecretsServiceдешифрует текущийPayloadEncrypted, маппит payload черезPassworkItemMapperв DTOPassworkCreateItemRequest.PassworkClient.CreateItemAsyncсоздаёт item в сейфе (DefaultVaultIdили путь изExternalPath, см. passwork.md § 7).- После успешного ответа
IntegrationSecretsобновляется атомарно:PayloadEncrypted = NULL,ExternalServiceId = ...,ExternalSecretId = .... - Аудит:
MigratedToExternalсExternalServiceId/ExternalSecretIdв payload-дельте.
Запрос отклоняется, если у Passwork-инстанса SupportsWrite = 0 либо PassworkValidator.LastResult = Critical.
13.3. Поток «обратная миграция» (MigratedToLocal)¶
POST api/admin/secrets/{serviceKey}/migrate-to-local(без body).IntegrationSecretsServiceчитает item из внешнего хранилища черезPassworkExternalSecretsClient.GetSecretAsync.- Payload шифруется штатным
IEncryptionServiceи пишется вPayloadEncrypted. ExternalServiceId = NULL,ExternalSecretId = NULL.- Item во внешнем хранилище не удаляется (защита от случайной потери — удаление остаётся ручной операцией в UI хранилища).
- Аудит:
MigratedToLocal.
13.4. Чтение и запись alias-секретов в runtime¶
SecretsProvider при чтении проверяет ExternalServiceId:
- NULL → LocalSecretsProvider дешифрует PayloadEncrypted.
- NOT NULL → ExternalSecretsProviderRouter берёт IExternalSecretsClient по ServiceType инстанса и вызывает GetSecretAsync(externalSecretId).
Запись по alias (UTILS.set_secret_value из смарт-скриптов и Admin UI) симметричная: IExternalSecretsClient.UpdateSecretAsync → PassworkClient.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.