08558df8ee
Accessibility (Pa11y live) / a11y (push) Has been cancelled
Инцидент 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>
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/подтверждение почты сломаются.'
|
||
);
|
||
}
|
||
});
|