Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Як ізолювати дані орендарів у багатоорендному застосунку: глобальні області видимості чи Row-Level Security у PostgreSQL?

У багатоорендному (multi-tenant) застосунку дані різних клієнтів-компаній лежать в одних таблицях з колонкою tenant_id. Найгірший можливий інцидент - витік даних між орендарями, тож ізоляція має бути системною, а не «не забути where в кожному запиті».

Рівень застосунку - глобальна область видимості Eloquent:

#[ScopedBy(TenantScope::class)]
class Invoice extends Model {}

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where($model->qualifyColumn('tenant_id'), Tenant::current()->id);
    }
}

Кожен Eloquent-запит до моделі автоматично обмежено поточним орендарем. Плюс автоматичне заповнення tenant_id при створенні.

Слабкі місця глобальних областей:

  • не працюють поза Eloquent: DB::table('invoices'), сирі запити, звіти, деякі пакети;
  • withoutGlobalScopes() - один виклик знімає захист;
  • черги, команди, планувальник - немає HTTP-запиту, і «поточний орендар» має бути встановлений явно (інакше область або падає, або - гірше - не застосовується);
  • зв'язки й підзапити (whereHas, join) можуть обійти область для пов'язаних таблиць;
  • унікальні індекси й exists-валідація без tenant_id - перевірка «email уже зайнятий» по всіх орендарях розкриває існування даних.

Рівень бази - Row-Level Security (RLS) у PostgreSQL:

ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON invoices
    USING (tenant_id = current_setting('app.tenant_id')::bigint)
    WITH CHECK (tenant_id = current_setting('app.tenant_id')::bigint);
DB::statement("SELECT set_config('app.tenant_id', ?, true)", [$tenant->id]);   // true - лише в межах транзакції

Тепер будь-який запит - Eloquent, сирий SQL, звіт - бачить лише рядки поточного орендаря. Помилка в коді застосунку не перетвориться на витік.

Підводні камені RLS:

  • власник таблиці й суперкористувачі обходять RLS за замовчуванням - потрібен FORCE ROW LEVEL SECURITY і окрема роль застосунку без прав власника та без BYPASSRLS;
  • пули з'єднань (PgBouncer у режимі транзакцій, постійні з'єднання Octane): значення, встановлене для сесії, «перетікає» в наступний запит іншого орендаря. Тому - set_config(..., true) / SET LOCAL у транзакції на кожен запит;
  • міграції, адмінка й фонові задачі між орендарями потребують окремої ролі з явним обходом;
  • продуктивність: умова політики додається до кожного запиту - потрібні індекси з tenant_id першою колонкою.

Підсумок: глобальна область - зручність і перший рівень, RLS - гарантія на рівні бази. Для даних, де витік між клієнтами неприпустимий, - обидва, плюс тести «орендар A не бачить даних орендаря B» для кожної моделі.

Докладніше в документації: PostgreSQL: Row Security Policies

Схожі питання