Files
portal/app/tests/Feature/Database/RlsGucHardeningGuardTest.php
T

59 lines
3.6 KiB
PHP
Raw Normal View History

<?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/подтверждение почты сломаются.'
);
}
});