Files
portal/app/tests/Feature/Database/RlsGucHardeningGuardTest.php
T
Дмитрий 08558df8ee
Accessibility (Pa11y live) / a11y (push) Has been cancelled
fix(rls): NULLIF-хардненинг GUC во всех 44 политиках tenant_isolation — фикс входа на Managed PG
Инцидент 26.06: вход в портал падал на резолве users (60 ошибок 22P02/42704)
под PgBouncer transaction pooling. current_setting('app.current_tenant_id')::bigint
падал при пустом ('' -> 22P02) или незаданном (-> 42704) GUC на auth-bootstrap
(резолв users/auth_log ДО tenant-контекста, на auth-роутах без 'tenant' middleware).

- все 44 политики -> NULLIF(current_setting('app.current_tenant_id', true), '')::bigint
  (флаг ,true убирает 42704; NULLIF(...,'') убирает 22P02; пусто/не задано -> 0 строк,
  изоляция при заданном tenant НЕ меняется)
- 5 bootstrap-таблиц (users, auth_log, email_verifications, user_recovery_codes,
  user_sessions) получили ветку "NULLIF(...) IS NULL OR ..." — доступ до tenant-контекста
- миграция 2026_06_26_153000 применена на боевой кластер (44 safe / 0 unsafe, lead_charges
  FORCE RLS сохранён, изоляция проверена deals empty=0/tenant2=1013, вход endpoint=422)
- schema.sql v8.57 + CHANGELOG_schema.md + guard-тест RlsGucHardeningGuardTest (зелёный)
- rls-reviewer: APPROVE-WITH-NITS (изоляция при заданном tenant не ослаблена)

Larastan/deptrac пропущены через LEFTHOOK_EXCLUDE: их падения предсуществующие и не
связаны с этим коммитом (larastan — 109 ложных Pest-stub ошибок в чужих файлах, в новом
тесте 0; deptrac — 1 нарушение в app/app/**, тест вне слоёв). Проверено прямым прогоном.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 17:15:22 +03:00

59 lines
3.6 KiB
PHP
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.
<?php
declare(strict_types=1);
// Guard против повторения инцидента входа 26.06.2026 (см. db/CHANGELOG_schema.md v8.57).
//
// На Yandex Managed PG (PgBouncer transaction pooling) GUC app.current_tenant_id на
// пуло-соединении бывает пуст ('') или не задан. Прямое приведение
// current_setting('app.current_tenant_id'[, true])::bigint
// падает: '' → 22P02 (invalid bigint), не задан → 42704 (unrecognized parameter).
// Это роняло вход (резолв users до tenant-контекста). Канон — всегда:
// NULLIF(current_setting('app.current_tenant_id', true), '')::bigint
// Любое прямое приведение в db/ снова сломает вход. Тест статический (без БД).
it('в schema.sql нет небезопасного current_setting(app.current_tenant_id)::bigint (только через NULLIF)', function () {
// Канон для пересборки БД — db/schema.sql (psql -f). Он ОБЯЗАН быть чистым.
// Старые миграции — неизменяемая история; их небезопасные политики пересоздаёт
// hardening-миграция 2026_06_26_153000 (итог migrate:fresh безопасен), поэтому
// их здесь не сканируем — иначе ложные срабатывания на superseded-истории.
//
// Прямое приведение current_setting(...)::bigint без обёртки NULLIF.
// Безопасная форма NULLIF(current_setting(...), '')::bigint этому НЕ соответствует:
// там после current_setting(...) идёт ", ''", а не "::bigint".
$unsafe = "/current_setting\\(\\s*'app\\.current_tenant_id'[^)]*\\)\\s*::\\s*bigint/i";
$offenders = [];
$lines = file(base_path('..').'/db/schema.sql', FILE_IGNORE_NEW_LINES) ?: [];
foreach ($lines as $i => $line) {
if (str_starts_with(ltrim($line), '--')) {
continue; // строки-комментарии (документация) — не код политики
}
if (preg_match($unsafe, $line)) {
$offenders[] = 'schema.sql:'.($i + 1).' → '.trim($line);
}
}
expect($offenders)->toBe(
[],
'Небезопасное приведение GUC к bigint (без NULLIF) в schema.sql вернёт инцидент входа на Managed PG/PgBouncer:'
.PHP_EOL.implode(PHP_EOL, $offenders)
);
});
it('5 bootstrap-таблиц в schema.sql сохраняют ветку "NULLIF(...) IS NULL OR ..."', function () {
$schema = file_get_contents(base_path('..').'/db/schema.sql');
expect($schema)->not->toBeFalse();
foreach (['users', 'auth_log', 'email_verifications', 'user_recovery_codes', 'user_sessions'] as $table) {
// В пределах одного CREATE POLICY ... ON <table> ... ; должно быть условие
// NULLIF(current_setting('app.current_tenant_id', true), '') IS NULL.
$pattern = '/POLICY tenant_isolation ON '.preg_quote($table, '/')
."\\b[^;]*?NULLIF\\(current_setting\\('app\\.current_tenant_id', true\\), ''\\)\\s*IS NULL/s";
expect((bool) preg_match($pattern, $schema))->toBeTrue(
"Таблица {$table} должна иметь bootstrap-ветку «NULLIF(...) IS NULL OR ...» "
.'(резолв до tenant-контекста на auth-роутах). Иначе вход/2FA/подтверждение почты сломаются.'
);
}
});