Что такое row-level security (RLS) в PostgreSQL?
Что такое 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), из-за чего контекст утекает между транзакциями в пуле соединений


