5fe6620bcf
Поправка по факту кода: реально сломаны только 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>
155 lines
11 KiB
Markdown
155 lines
11 KiB
Markdown
# Дизайн: восстановление доступа 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-дверь админки.
|