Реализация Row-Level Security для мультитенантного веб-приложения

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация Row-Level Security для мультитенантного веб-приложения
Сложная
~3-5 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Row-Level Security для мультиарендного приложения

Row-Level Security (RLS) — механизм PostgreSQL, позволяющий ограничить доступ к строкам таблицы прямо на уровне СУБД. Даже если приложение ошибётся и не передаст WHERE tenant_id = ?, PostgreSQL автоматически применит политику. RLS — второй контур защиты данных tenant'ов, независимый от ORM.

Как работает RLS

-- Включить RLS для таблицы
ALTER TABLE articles ENABLE ROW LEVEL SECURITY;

-- По умолчанию владелец таблицы (superuser) обходит RLS
-- Для проверки политик даже для owner:
ALTER TABLE articles FORCE ROW LEVEL SECURITY;

-- Политика: строка видна, если tenant_id совпадает с контекстом
CREATE POLICY tenant_isolation_select ON articles
    FOR SELECT
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Политика INSERT: нельзя вставить строку с чужим tenant_id
CREATE POLICY tenant_isolation_insert ON articles
    FOR INSERT
    WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Объединить SELECT/INSERT/UPDATE/DELETE
CREATE POLICY tenant_isolation ON articles
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid)
    WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);

current_setting('app.current_tenant_id') — параметр сессии, который приложение устанавливает перед запросами.

Установка контекста в приложении

// Laravel — установка tenant context в middleware
class SetTenantContext
{
    public function handle(Request $request, Closure $next): Response
    {
        $tenant = app('tenant'); // установлен ранее

        // Установить PostgreSQL session variable
        DB::statement(
            "SELECT set_config('app.current_tenant_id', ?, false)",
            [$tenant->id]
        );

        return $next($request);
    }
}

false в третьем параметре set_config — значение применяется только в текущей транзакции. true — на всю сессию. Для connection pooling (PgBouncer) безопаснее false — при возврате соединения в пул значение сбрасывается.

PgBouncer и RLS

PgBouncer в transaction mode сбрасывает session-level переменные между транзакциями — это хорошо для безопасности, но требует устанавливать app.current_tenant_id в начале каждой транзакции:

DB::transaction(function () use ($tenant) {
    DB::statement(
        "SELECT set_config('app.current_tenant_id', ?, true)",
        [$tenant->id]
    );

    // Все запросы внутри транзакции защищены RLS
    Article::create([...]);
    Comment::create([...]);
});

Разные политики для ролей

-- Суперадмин видит все строки
CREATE POLICY superadmin_all ON articles
    FOR ALL
    USING (current_setting('app.is_superadmin', true) = 'true');

-- Пользователи видят только свои и опубликованные статьи своего tenant'а
CREATE POLICY user_select ON articles
    FOR SELECT
    USING (
        tenant_id = current_setting('app.current_tenant_id')::uuid
        AND (
            author_id = current_setting('app.current_user_id')::uuid
            OR status = 'published'
        )
    );

-- Редакторы могут видеть черновики в своём tenant'е
CREATE POLICY editor_select ON articles
    FOR SELECT
    USING (
        tenant_id = current_setting('app.current_tenant_id')::uuid
        AND current_setting('app.current_role', true) = 'editor'
    );

Несколько политик для одной команды (SELECT) объединяются через OR (permissive) или через AND (restrictive).

Restrictive политики

Permissive (по умолчанию): доступ разрешён, если ЛЮБАЯ политика выполнена. Restrictive: доступ разрешён, если ВСЕ restrictive политики выполнены.

-- Hard limit: удалённые аккаунты не видят ничего вне зависимости от других политик
CREATE POLICY no_deleted_tenant ON articles
    AS RESTRICTIVE
    USING (
        NOT EXISTS (
            SELECT 1 FROM tenants
            WHERE id = current_setting('app.current_tenant_id')::uuid
            AND deleted_at IS NOT NULL
        )
    );

Обход RLS для системных операций

-- Специальная роль без RLS (для миграций, аналитики)
CREATE ROLE app_migrations BYPASSRLS;
CREATE ROLE app_analytics BYPASSRLS;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_analytics;

-- В приложении: два пула соединений
-- app_user — обычная роль с RLS
-- app_analytics — роль с BYPASSRLS для аналитических запросов
// Laravel: отдельный connection для аналитики
DB::connection('analytics')->select('SELECT COUNT(*) FROM articles GROUP BY tenant_id');

Тестирование RLS политик

-- Тест от имени tenant_a
SET app.current_tenant_id = 'tenant-a-uuid';
SELECT count(*) FROM articles; -- должна видеть только статьи tenant_a

-- Тест попытки вставить в чужой tenant
SET app.current_tenant_id = 'tenant-a-uuid';
INSERT INTO articles (tenant_id, title)
VALUES ('tenant-b-uuid', 'Попытка взлома'); -- ERROR: new row violates row-level security policy
// PHPUnit — тест на утечку данных между tenant'ами
public function test_tenant_isolation(): void
{
    $tenantA = Tenant::factory()->create();
    $tenantB = Tenant::factory()->create();

    Article::factory()->count(5)->create(['tenant_id' => $tenantA->id]);
    Article::factory()->count(3)->create(['tenant_id' => $tenantB->id]);

    // Авторизуемся как tenant A
    DB::statement("SELECT set_config('app.current_tenant_id', ?, false)", [$tenantA->id]);

    $articles = Article::all();

    $this->assertCount(5, $articles);
    $this->assertTrue($articles->every(fn($a) => $a->tenant_id === $tenantA->id));
}

Производительность

RLS добавляет условие к каждому запросу — индекс на tenant_id обязателен:

-- Составной индекс для типичных запросов
CREATE INDEX articles_tenant_status_idx ON articles(tenant_id, status);
CREATE INDEX articles_tenant_created_idx ON articles(tenant_id, created_at DESC);

-- Частичный индекс для активных записей
CREATE INDEX articles_active_idx ON articles(tenant_id, created_at DESC)
    WHERE deleted_at IS NULL;

EXPLAIN ANALYZE покажет применение RLS-фильтра — убедитесь, что используется Index Scan, не Seq Scan.

Сроки

RLS политики на все таблицы, middleware установки контекста, тесты изоляции, обходная роль для миграций: 1 неделя. С restrictive политиками для удалённых аккаунтов, BYPASSRLS для аналитики, нагрузочное тестирование производительности: 2 недели.