Поправка по факту кода: реально сломаны только AdminTenantsController и AdminBillingController (ходят под default crm_app_user); Incidents/Pd/ SupplierIntegration/Impersonation уже используют pgsql_supplier и работают. План: connection pgsql_admin + middleware UseAdminConnection (admin-db). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
Дизайн: восстановление доступа SaaS-админки к данным после переезда на Managed PG (Путь А)
Дата: 2026-06-27 Автор: Claude (контроллер) + Дмитрий (владелец) Статус: approved (способ A), готов к плану
Проблема
После переезда боевой базы на управляемый PostgreSQL (Путь А, 26.06.2026)
SaaS-админка liderra.ru/admin в разделах «Тенанты» и «Биллинг» перестала
показывать данные.
Уточнение по факту кода (27.06.2026)
Первичный замер делался по голым таблицам под ролью crm_app_user и переоценил
масштаб. По факту кода admin-контроллеры делятся на две группы:
- Уже используют bypass-подключение
pgsql_supplier(рольcrm_supplier_worker, видит все тенанты) → работают:AdminIncidentsController,AdminPdSubjectRequestsController,AdminSupplierIntegrationController,ImpersonationController. - Читают глобальные (не tenant-scoped) таблицы под default-подключением →
работают:
AdminPricingTiersController(tariff_plans),AdminSystemSettingsController(system_settings),AdminSuppliersController(supplier_projects/project_suppliers). - Ходят под default-подключением
pgsql(рольcrm_app_user) по tenant-scoped таблицам → СЛОМАНЫ:AdminTenantsController— список тенантов, карточка тенанта (users/projects/deals/balance), правка баланса (updateBalance).AdminBillingController— агрегаты биллинга, refund, смена статуса/тарифа.
Замер таблиц (боевая база)
| Таблица | crm_app_user (default) |
bypass-роль | Затронутый раздел |
|---|---|---|---|
tenants |
0 | 6 | Тенанты, Биллинг — СЛОМАНЫ |
balance_transactions |
0 | 919 | Биллинг — СЛОМАН |
deals / projects |
0 / 0 | 1016 / 122 | Карточка тенанта — СЛОМАНА |
tariff_plans |
4 | 4 | Тарифная сетка — ок |
system_settings |
46 | 46 | Система — ок |
supplier_projects |
401 | 401 | Проекты поставщика — ок |
Данные целы (деньги/сделки/клиенты на месте) — сломан только доступ двух
контроллеров, ходящих под crm_app_user.
Корень
При переезде BYPASSRLS заменён на политики srv_bypass (cmd=ALL, qual=true),
выданные ролям crm_admin_user, crm_supplier_worker, crm_migrator.
Админка же ходит в базу под crm_app_user, которая:
- подпадает под
tenants_self_isolation(id = current_setting('app.current_tenant_id')) → на админ-страницах GUC не выставлен → 0 строк; - не имеет табличного GRANT на чисто-админские таблицы (incidents_log, saas_admin_audit_log, impersonation_tokens, pd_subject_requests) → 42501.
Роль crm_admin_user — штатно предназначенная для админки — уже полностью
готова на кластере:
- политика
srv_bypass(видит все тенанты); - SELECT/INSERT/UPDATE на все админ-таблицы (проверено
has_table_privilege); canlogin=true, пароль в Lockboxliderra-secretsи/home/ubuntu/liderra-secrets.txt(ключcrm_admin_user).
Не хватает единственного: подключения в приложении под этой ролью.
Решение (способ A — connection swap на входе в админку)
1. Новое подключение pgsql_admin
В config/database.php добавить pgsql_admin = базовый $pgsqlConnection
- override
username/passwordиз новых env-ключейDB_ADMIN_USERNAME=crm_admin_user,DB_ADMIN_PASSWORD=<из Lockbox>. Паттерн идентичен существующемуpgsql_supplier.
2. Переключатель только для админ-группы
Новый middleware UseAdminConnection (alias admin-db) добавляется в группу
Route::middleware('saas-admin') после saas-admin (EnsureSaasAdmin),
на время обработки запроса делает DB::setDefaultConnection('pgsql_admin') и
восстанавливает прежнее значение в finally.
- Восстановление в
finallyобязательно: в Pest-тестах несколько «запросов» идут в одном процессе → без restore default-подключение утечёт в соседние тесты. - Tenant-facing запросы — отдельная middleware-группа (
tenant) → остаются наpgsql(crm_app_user) → изоляция клиентов не тронута. - Переключатель меняет default, поэтому чинит ровно те контроллеры, что
ходят под default (
AdminTenantsController,AdminBillingController) — это и есть сломанный набор. Контроллеры, явно прибитые кpgsql_supplier(Incidents/Pd/SupplierIntegration/Impersonation), переключатель не трогает — они уже работают и продолжат работать под своей ролью.
Middleware ставится после saas-admin, чтобы проверка impersonation/гейта прошла
до смены подключения.
Швы (обязательны к проверке в плане)
- Изоляция клиентов не должна пострадать. Обычный кабинет остаётся на
crm_app_user. Проверить агентомrls-reviewer+ регресс-тест: tenant-facing эндпоинт видит только свой тенант. - Hardcoded-подключения (учтены).
AdminIncidentsController,AdminPdSubjectRequestsController,AdminSupplierIntegrationController,ImpersonationControllerявно прибиты кpgsql_supplier— переключатель их не затрагивает, они уже работают. Это осознанно оставляем как есть (YAGNI; рабочий код не трогаем). Известный долг: набор admin-доступа неоднороден (часть подcrm_supplier_worker, новые два — подcrm_admin_user) — унификация наcrm_admin_userвозможна позже отдельным безопасным шагом. Проверить отсутствиеprotected $connectionв моделях, которые читаетAdminTenantsController/AdminBillingController(Tenant,BalanceTransaction) — если есть пин наpgsql, он обойдёт переключатель (на момент аудита — нет). - Фоновые джобы из админки выполняются в worker'е под обычным default
(
pgsql/pgsql_supplier) — убедиться, что admin-действия, ставящие джобы, не полагаются на admin-подключение внутри джобы. - Тестовый шов (главный). dev/test-база работает под суперпользователем
postgres, который игнорирует RLS → баг не всплывал в тестах. Нужен тест, реально проверяющий поведение под ролями (черезSET ROLE/ отдельное подключение), иначе фикс «зелёный в тестах, мёртвый на проде». - Запись под admin-ролью. Права INSERT/UPDATE подтверждены; протестировать реальную правку (баланс тенанта, system_settings, запись saas_admin_audit_log).
- Impersonation.
EnsureSaasAdminзапрещает вход в админ-зону при активной impersonation — переключатель не должен это ослаблять (ставить ПОСЛЕ проверки impersonation в цепочке middleware).
Тестирование
Автоматизированное (портативно, любой стенд):
- Connection-config:
pgsql_adminопределён, username =crm_admin_user(через env с fallback на default — как уpgsql_supplier). - Middleware unit:
UseAdminConnectionставит default наpgsql_adminи восстанавливает прежнее значение после запроса (включая ветку с исключением). - Routing: admin-группа
/api/admin/*имеет middlewareadmin-dbв пайплайне.
Шов №4 (честно): dev/test-база под суперпользователем postgres игнорирует
RLS, поэтому факт «cross-tenant данные видны» автотестом на dev не доказывается.
Реальная проверка — на боевой/изолированной базе под ролями:
- live-приёмка: тот же замер «default-роль vs admin-роль» (скрипт из раздела
«Проблема») после выката показывает, что
AdminTenantsControllerподcrm_admin_userвидит все тенанты; - изоляция: tenant-facing эндпоинт под
crm_app_userпо-прежнему видит только свой тенант (regress); - write: правка баланса тенанта проходит и пишет
saas_admin_audit_log.
Выкат и откат
- Код: gitea → прод (rsync overlay, БД не трогается).
- Прод-настройка: добавить
DB_ADMIN_USERNAME/DB_ADMIN_PASSWORDв.env(из Lockbox),php artisan config:cache. - Перед выкатом — отдельное разрешение владельца +
prod-deploy-validator. - Откат: убрать middleware-переключатель (поведение возвращается к текущему, которое и так сломано) / revert коммита.
Не делаем (YAGNI)
- Не трогаем настоящий saas-admin SSO (Yandex 360) — отдельный гейт Б-1/DO-4.
- Не меняем RLS-политики и роли на кластере — они уже корректны.
- Не правим nginx-дверь админки.