Что такое row-level security (RLS) в PostgreSQL?

SeniorPostgreSQL · Backend·Обновлено 6 августа 2026
Коротко
Row-Level Security (RLS) — механизм PostgreSQL, позволяющий ограничивать доступ к строкам таблицы на уровне СУБД в зависимости от текущего пользователя или контекста сессии. Политики RLS автоматически добавляются к каждому запросу как дополнительные условия WHERE.

Что такое Row-Level Security

Row-Level Security (RLS) — встроенный механизм безопасности PostgreSQL, позволяющий контролировать доступ к отдельным строкам таблицы на уровне самой базы данных, а не в прикладном коде. RLS работает прозрачно: PostgreSQL автоматически добавляет условия фильтрации к каждому SELECT, INSERT, UPDATE и DELETE.

Важное отличие от простых прав доступа (GRANT/REVOKE) — те управляют доступом на уровне объектов (таблиц, схем), тогда как RLS управляет доступом на уровне конкретных строк внутри таблицы.

Как включить RLS

RLS включается на уровне таблицы командой ALTER TABLE ... ENABLE ROW LEVEL SECURITY. После включения без явных политик таблица становится недоступной для всех пользователей кроме суперпользователя и владельца таблицы (режим deny-all по умолчанию).

-- Включаем RLS на таблице
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Владелец таблицы тоже ограничен политиками
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

Создание политик

Политика определяет условие, которое должна удовлетворять строка, чтобы текущий пользователь мог с ней работать.

-- Политика: пользователь видит только свои заказы
CREATE POLICY orders_user_isolation
  ON orders
  FOR ALL
  TO authenticated_users
  USING (user_id = current_user_id());

-- Политика только для SELECT
CREATE POLICY orders_read
  ON orders
  FOR SELECT
  USING (user_id = current_setting('app.current_user_id')::int);

-- Политика для INSERT с проверкой нового значения
CREATE POLICY orders_insert
  ON orders
  FOR INSERT
  WITH CHECK (user_id = current_setting('app.current_user_id')::int);

USING — фильтр для чтения и изменения существующих строк. WITH CHECK — проверка для записываемых/изменяемых данных.

Передача контекста через set_config

При работе с пулом соединений (pgBouncer, pgpool) нельзя использовать отдельного DB-пользователя на каждого клиента. Популярный паттерн — передавать контекст через SET LOCAL:

-- В начале транзакции приложение устанавливает контекст
BEGIN;
SELECT set_config('app.current_user_id', '42', true); -- true = локально для транзакции
SELECT * FROM orders; -- вернёт только строки с user_id = 42
COMMIT;

Политики для ролей и иерархии доступа

-- Администраторы видят всё
CREATE POLICY orders_admin
  ON orders
  FOR ALL
  TO admin_role
  USING (true);

-- Менеджеры видят заказы своего отдела
CREATE POLICY orders_manager
  ON orders
  FOR SELECT
  TO manager_role
  USING (
    department_id IN (
      SELECT department_id FROM managers WHERE user_id = current_setting('app.current_user_id')::int
    )
  );

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

Политики RLS добавляют условия к каждому запросу, поэтому крайне важно иметь индексы на колонках, используемых в USING-выражениях. Рекомендуется проверять план через EXPLAIN — условие политики должно попадать под index scan, а не seq scan.

Ограничения и важные нюансы

  • Суперпользователь (superuser) и пользователь с атрибутом BYPASSRLS политики не соблюдают.
  • FORCE ROW LEVEL SECURITY заставляет политики применяться и к владельцу таблицы.
  • При использовании SECURITY DEFINER-функций политики применяются в контексте владельца функции, а не вызывающего — это частый источник уязвимостей.
  • RLS не заменяет шифрование: данные физически хранятся без ограничений, политики работают только на уровне запросов.

Что хочет услышать интервьюер

Понимание разницы между табличными правами (GRANT) и построчными политиками (RLS) — это принципиально разные уровни контроля доступа

Знание ключевых клауз CREATE POLICY: USING (фильтрация существующих строк) и WITH CHECK (проверка записываемых данных)

Понимание паттерна передачи пользовательского контекста через set_config/current_setting при использовании пула соединений

Осведомлённость о влиянии RLS на производительность и необходимости индексов на полях в условиях политик

Знание исключений: суперпользователь и BYPASSRLS обходят политики; SECURITY DEFINER-функции применяют политики владельца

Пример: Полный пример: мультитенантная таблица с RLS

-- Создаём таблицу заказов
CREATE TABLE orders (
  id         SERIAL PRIMARY KEY,
  user_id    INT NOT NULL,
  product    TEXT NOT NULL,
  amount     NUMERIC(10, 2) NOT NULL
);

-- Индекс на поле, используемом в политике — обязателен для производительности
CREATE INDEX orders_user_id_idx ON orders (user_id);

-- Включаем RLS, в том числе для владельца таблицы
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

-- Политика SELECT: видим только свои заказы
CREATE POLICY orders_select
  ON orders
  FOR SELECT
  USING (user_id = current_setting('app.current_user_id')::int);

-- Политика INSERT: нельзя создать заказ от имени другого пользователя
CREATE POLICY orders_insert
  ON orders
  FOR INSERT
  WITH CHECK (user_id = current_setting('app.current_user_id')::int);

-- Политика UPDATE: можно изменять только свои строки
CREATE POLICY orders_update
  ON orders
  FOR UPDATE
  USING (user_id = current_setting('app.current_user_id')::int)
  WITH CHECK (user_id = current_setting('app.current_user_id')::int);

-- Администраторы видят и изменяют всё
CREATE POLICY orders_admin
  ON orders
  FOR ALL
  TO admin_role
  USING (true)
  WITH CHECK (true);

-- Приложение устанавливает контекст перед запросами
BEGIN;
SELECT set_config('app.current_user_id', '42', true);
-- Этот SELECT вернёт только заказы пользователя 42
SELECT * FROM orders;
COMMIT;

Пример: Установка контекста RLS в Node.js (pg)

import { Pool } from 'pg';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

async function getOrdersForUser(userId: number) {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');

    // Устанавливаем контекст текущего пользователя — true означает LOCAL (только для транзакции)
    await client.query(`SELECT set_config('app.current_user_id', $1, true)`, [String(userId)]);

    // RLS автоматически отфильтрует чужие строки
    const result = await client.query('SELECT * FROM orders');

    await client.query('COMMIT');
    return result.rows;
  } catch (err) {
    await client.query('ROLLBACK');
    throw err;
  } finally {
    client.release();
  }
}

Типичные ошибки

Путают USING и WITH CHECK: думают, что одного USING достаточно для защиты INSERT/UPDATE, хотя WITH CHECK проверяет записываемые данные

Забывают про FORCE ROW LEVEL SECURITY — владелец таблицы по умолчанию обходит все политики без этой опции

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

Считают, что RLS защищает от суперпользователя — на самом деле суперпользователь и роли с BYPASSRLS игнорируют все политики

Используют set_config без режима LOCAL (второй параметр true), из-за чего контекст утекает между транзакциями в пуле соединений

Лучшие курсы по теме

изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Node.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее