# Дизайн: восстановление доступа 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-дверь админки.
