Files
portal/app/tests/Feature/RlsSmokeTest.php
T

116 lines
4.9 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
/**
* CTO-13: RLS smoke-test.
*
* Проверяет ключевые сценарии Row Level Security из Прил. И «Часть Г.1»:
* - Кейс 1: SET LOCAL app.current_tenant_id изолирует SELECT по tenant_id.
* - Кейс 4: WITH CHECK блокирует INSERT в чужой тенант.
*
* Кейсы 2-3 (PgBouncer pooling, job retry) — отдельно в production-окружении
* через Прил. И Г.1 (PgBouncer на native Windows-стеке нет).
*
* Кейс 5 (REVOKE на 6 saas-таблицах) — требует роли `crm_app_user` из
* db/00_create_roles.sql + db/02_grants.sql, на dev superuser обходит REVOKE.
*
* Стратегия: для эмуляции crm_app_user на dev-машине создаётся NOLOGIN-роль
* `testing_rls_user` без BYPASSRLS. SET LOCAL ROLE внутри транзакции —
* сбрасывается ROLLBACK'ом DatabaseTransactions.
*/
uses(DatabaseTransactions::class);
beforeEach(function () {
// Идемпотентное создание testing-роли (роль = глобальный объект, не Rolled Back).
DB::unprepared(<<<'SQL'
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'testing_rls_user') THEN
CREATE ROLE testing_rls_user NOLOGIN;
GRANT USAGE ON SCHEMA public TO testing_rls_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO testing_rls_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO testing_rls_user;
END IF;
END
$$;
SQL);
// 2 тенанта для проверки изоляции
$this->tenant1Id = DB::table('tenants')->insertGetId([
'subdomain' => 'rls-tenant-a-'.uniqid(),
'organization_name' => 'RLS Tenant A',
'contact_email' => 'a@rls-test.local',
'api_key_limit' => 5,
]);
$this->tenant2Id = DB::table('tenants')->insertGetId([
'subdomain' => 'rls-tenant-b-'.uniqid(),
'organization_name' => 'RLS Tenant B',
'contact_email' => 'b@rls-test.local',
'api_key_limit' => 5,
]);
$this->project1Id = DB::table('projects')->insertGetId([
'tenant_id' => $this->tenant1Id,
'name' => 'Project A1',
]);
$this->project2Id = DB::table('projects')->insertGetId([
'tenant_id' => $this->tenant2Id,
'name' => 'Project B1',
]);
DB::table('deals')->insert([
['tenant_id' => $this->tenant1Id, 'project_id' => $this->project1Id, 'phone' => '+79000010001', 'received_at' => now()],
['tenant_id' => $this->tenant1Id, 'project_id' => $this->project1Id, 'phone' => '+79000010002', 'received_at' => now()],
['tenant_id' => $this->tenant2Id, 'project_id' => $this->project2Id, 'phone' => '+79000020001', 'received_at' => now()],
['tenant_id' => $this->tenant2Id, 'project_id' => $this->project2Id, 'phone' => '+79000020002', 'received_at' => now()],
]);
});
test('кейс 1: SET LOCAL app.current_tenant_id изолирует SELECT в deals', function () {
DB::statement('SET LOCAL ROLE testing_rls_user');
DB::statement("SET LOCAL app.current_tenant_id = '{$this->tenant1Id}'");
$allDealsVisible = DB::table('deals')->count();
$foreignDealsVisible = DB::table('deals')->where('tenant_id', $this->tenant2Id)->count();
expect($allDealsVisible)->toBe(2);
expect($foreignDealsVisible)->toBe(0);
});
test('кейс 1: переключение tenant_id показывает только свои deals', function () {
DB::statement('SET LOCAL ROLE testing_rls_user');
DB::statement("SET LOCAL app.current_tenant_id = '{$this->tenant2Id}'");
$allDealsVisible = DB::table('deals')->count();
$foreignDealsVisible = DB::table('deals')->where('tenant_id', $this->tenant1Id)->count();
expect($allDealsVisible)->toBe(2);
expect($foreignDealsVisible)->toBe(0);
});
test('кейс 1: RLS работает на projects и users (не только deals)', function () {
DB::statement('SET LOCAL ROLE testing_rls_user');
DB::statement("SET LOCAL app.current_tenant_id = '{$this->tenant1Id}'");
$projectsVisible = DB::table('projects')->count();
$foreignProjectsVisible = DB::table('projects')->where('tenant_id', $this->tenant2Id)->count();
expect($projectsVisible)->toBe(1);
expect($foreignProjectsVisible)->toBe(0);
});
test('кейс 4: WITH CHECK блокирует INSERT projects с чужим tenant_id', function () {
DB::statement('SET LOCAL ROLE testing_rls_user');
DB::statement("SET LOCAL app.current_tenant_id = '{$this->tenant1Id}'");
expect(fn () => DB::table('projects')->insert([
'tenant_id' => $this->tenant2Id,
'name' => 'Hijack attempt',
]))->toThrow(QueryException::class);
});