Files
portal/docs/superpowers/specs/2026-06-27-admin-db-connection-path-a-design.md
T
Дмитрий 737d2e192b docs(админка): уточнённая спецификация + план фикса доступа через crm_admin_user
Поправка по факту кода: реально сломаны только 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>
2026-06-27 06:21:00 +03:00

11 KiB
Raw Blame History

Дизайн: восстановление доступа 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, пароль в Lockbox liderra-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/гейта прошла до смены подключения.

Швы (обязательны к проверке в плане)

  1. Изоляция клиентов не должна пострадать. Обычный кабинет остаётся на crm_app_user. Проверить агентом rls-reviewer + регресс-тест: tenant-facing эндпоинт видит только свой тенант.
  2. 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, он обойдёт переключатель (на момент аудита — нет).
  3. Фоновые джобы из админки выполняются в worker'е под обычным default (pgsql/pgsql_supplier) — убедиться, что admin-действия, ставящие джобы, не полагаются на admin-подключение внутри джобы.
  4. Тестовый шов (главный). dev/test-база работает под суперпользователем postgres, который игнорирует RLS → баг не всплывал в тестах. Нужен тест, реально проверяющий поведение под ролями (через SET ROLE / отдельное подключение), иначе фикс «зелёный в тестах, мёртвый на проде».
  5. Запись под admin-ролью. Права INSERT/UPDATE подтверждены; протестировать реальную правку (баланс тенанта, system_settings, запись saas_admin_audit_log).
  6. 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/* имеет middleware admin-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-дверь админки.