# Row Level Security в Supabase: как на самом деле закрыть таблицы

> Как устроены политики RLS, где в них чаще всего ошибаются и как проверить, что чужие строки действительно недоступны.

Published: 2025-04-17 · Source: https://subxcat.com/ru/articles/supabase-rls · Author: SUBXCAT

В Supabase браузер ходит в базу почти напрямую. Между ним и твоими таблицами нет бэкенда, который проверил бы права, — есть только Row Level Security. Поэтому RLS здесь не дополнительная защита, а единственная. Если политика написана неправильно, никакой другой слой ошибку не поймает.

Хорошая новость: правила простые. Плохая: ошибиться в них легко, а приложение при этом продолжает работать как ни в чём не бывало.

## Как это устроено

RLS включается на таблицу. Пока он выключен, таблицу видит любой, у кого есть публичный ключ, то есть весь интернет. Когда включён и политик нет, не видит никто, кроме сервисной роли. Политики открывают доступ обратно, строка за строкой.

У политики две части. `using` решает, какие существующие строки можно видеть, менять и удалять. `with check` решает, какие строки можно записать — при вставке и после обновления. Политики одной команды складываются через «или»: если одна разрешает всё, остальные уже не важны.

```sql
alter table notes enable row level security;

create policy "notes: read own" on notes
  for select to authenticated
  using ((select auth.uid()) = user_id);

create policy "notes: insert own" on notes
  for insert to authenticated
  with check ((select auth.uid()) = user_id);
```

Обёртка `(select auth.uid())` — не украшение: так Postgres вычисляет идентификатор один раз на запрос, а не на каждую строку. На больших таблицах разница заметна. И поставь индекс на `user_id` — политика по сути добавляет условие в каждый запрос.

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

То, что мы находим чаще всего:

- **RLS включён не везде.** Таблицу добавили позже, через миграцию или руками в редакторе, и про неё забыли. Список таблиц без RLS даёт `select tablename from pg_tables where schemaname = 'public' and not rowsecurity;`.
- **«Вошёл — значит можно».** Политика `using (auth.role() = 'authenticated')` пускает к чужим данным любого зарегистрированного. Регистрация бесплатная, так что это почти то же, что не закрывать вовсе.
- **Вставка без `with check`.** Читать чужое нельзя, а записать строку с чужим `user_id` — можно. Так появляются заказы и сообщения «от имени» другого человека.
- **Роль из `user_metadata`.** Эти поля человек меняет сам через клиентскую библиотеку. Роли и тарифы — только в `app_metadata` или в отдельной таблице, которую пишет сервер.
- **Представления.** Обычный `view` выполняется с правами владельца и обходит RLS базовых таблиц. На Postgres 15+ нужен `with (security_invoker = true)`, иначе представление открывает то, что таблица закрывает.
- **Функции `security definer`.** Их можно вызвать через RPC, и внутри RLS не действует. Каждая такая функция должна сама проверять, кто её вызвал.
- **Storage.** Файлы в бакетах защищаются политиками на `storage.objects`, отдельно от таблиц. Закрытые таблицы и открытый бакет со сканами паспортов — обычная история.

## Как проверить

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

Первый способ — через клиент. Заведи двух тестовых пользователей, из-под первого создай записи, из-под второго попробуй прочитать, изменить и удалить их по идентификатору. Затем попробуй вставить строку с `user_id` первого. Каждая попытка должна вернуть пустой результат или ошибку.

Второй способ — прямо в SQL, внутри транзакции: `set local role authenticated`, затем `set local request.jwt.claims` с нужным `sub`, затем обычные запросы. Так удобно гонять проверки в CI, не поднимая фронтенд. Для этого есть pgTAP, но хватит и набора SQL-файлов, которые падают при неожиданном результате.

Третье — проверка на полноту. Тест, который перечисляет все таблицы схемы `public` и падает, если у какой-то выключен RLS или нет ни одной политики, ловит самую частую ошибку — забытую новую таблицу.

## Где держать логику, которой мало RLS

Политики хороши для «чьё это». Для «можно ли это сделать сейчас» — хватает ли баланса, не закрыт ли заказ, не превышен ли лимит — им тесно. Такие операции лучше уносить в серверную функцию или edge function с сервисным ключом, а таблицу на запись для клиента закрыть совсем.

Если хочешь, чтобы кто-то со стороны попробовал обойти твои политики до того, как это сделают пользователи, SUBXCAT делает такую проверку и оставляет после неё тесты.
