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