Files
portal/docs/superpowers/specs/2026-06-27-admin-db-connection-path-a-design.md
T
Дмитрий 5fe6620bcf 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

155 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Дизайн: восстановление доступа 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-дверь админки.