Postgres RLS & Auth: Архитектура безопасности
Database / Security

Postgres RLS & Auth: Архитектура безопасности

Н
Никита
07 янв. 2026 г.

1. Введение: RLS — это твой единственный бэкенд

В архитектуре Supabase база данных берет на себя роль контроллера доступа.

  • Authentication (Кто ты?): Решает сервис GoTrue (Supabase Auth), выдавая JWT.

  • Authorization (Что тебе можно?): Решает Postgres через RLS, валидируя этот JWT.

Запомни: По умолчанию в Supabase всё закрыто. Если ты включил RLS на таблице и не добавил политик — никто (кроме service_role) не увидит ни байта.



2. Анатомия RLS: USING vs WITH CHECK

Это то, на чем валятся мидлы. У политики есть два контекста выполнения. Если ты их путаешь, у тебя будут дыры в безопасности.

Синтаксис:

SQL
CREATE POLICY "policy_name"
ON public.table_name
FOR [SELECT | INSERT | UPDATE | DELETE | ALL]
TO [authenticated | anon | public]
USING ( ...expression... )      -- Для фильтрации существующих строк
WITH CHECK ( ...expression... ); -- Для валидации новых данных

Разбор полетов:

  1. USING ( condition ):
  • Используется для SELECT, DELETE, UPDATE.
  • Определяет, какие строки видны пользователю.
  • Пример: "Покажи мне строки, где user_id равен моему ID".
  1. WITH CHECK ( condition ):
  • Используется для INSERT, UPDATE.
  • Определяет, можно ли записать/изменить строку с такими данными.
  • Пример: "Я пытаюсь создать пост. Проверь, что в поле author_id я подставил свой ID, а не ID соседа".

Опасный момент в UPDATE:
Команда UPDATE использует и то, и другое.

  • USING: Проверяет, имеешь ли ты право трогать эту строку.

  • WITH CHECK: Проверяет, имеешь ли ты право превратить строку в то, что отправляешь.

3. Базовые паттерны (Best Practices)

Включаем RLS для всех таблиц:

SQL
alter table public.todos enable row level security;
alter table public.profiles enable row level security;

Паттерн 1: "Каждый сам за себя" (User Data)

Самый частый кейс. Юзер видит и правит только свое.

SQL
create policy "CRUD own todos"
on public.todos
for all                         -- Применяется к select, insert, update, delete
to authenticated                -- Только для залогиненных
using ( auth.uid() = user_id )  -- Вижу только свои
with check ( auth.uid() = user_id ); -- Пишу только от своего имени

Обрати внимание: auth.uid() — это встроенная функция Supabase, извлекающая ID из JWT.

Паттерн 2: "Блог / Комментарии" (Public Read, Auth Write)

Читать могут все (даже гости), писать — только авторы.

SQL
-- Политика для чтения (гости + юзеры)
create policy "Public read"
on public.posts
for select
to public                       -- 'public' включает и anon, и authenticated
using ( true );                 -- Доступно всем

-- Политика для авторов (создание)
create policy "Authors can create"
on public.posts
for insert
to authenticated
with check ( auth.uid() = author_id );

-- Политика для авторов (редактирование/удаление)
create policy "Authors can edit own"
on public.posts
for update
to authenticated
using ( auth.uid() = author_id )
with check ( auth.uid() = author_id ); -- Нельзя сменить автора при апдейте

4. Сложные сценарии и Performance (SaaS / B2B)

Когда у вас появляется "Организация" или "Команда", простой auth.uid() перестает работать.

Проблема: JOIN в RLS убивает базу

Допустим, вы хотите проверить: "Пользователь состоит в организации, которой принадлежит этот документ".

Плохой код (Anti-pattern):

SQL
-- НЕ ДЕЛАЙТЕ ТАК НА HIGH LOAD
create policy "Access via Organization"
on public.documents
for select
using (
  exists (
    select 1 from public.organization_members om
    where om.org_id = documents.org_id
    and om.user_id = auth.uid()
  )
);

Почему это плохо: На каждую строку документа выполняется подзапрос к таблице organization_members. Если вы запрашиваете 1000 документов, вы делаете 1000 лишних селектов. Это положит базу.

Решение 1: Денормализация (Прагматичный подход)

Если пользователь редко меняет организации, проще хранить массив его org_ids прямо в JWT (через Custom Claims, как обсуждали ранее) или в кэширующей таблице.

Решение 2: Security Definer функция (Чистый подход)

Если логика сложная, выносим её в функцию, которая выполняется эффективно (по индексу).

SQL
create or replace function public.has_org_access(_org_id uuid)
returns boolean as $$
begin
  return exists (
    select 1 from public.organization_members
    where org_id = _org_id and user_id = auth.uid()
  );
end;
$$ language plpgsql security definer stable; -- STABLE кэширует результат в рамках транзакции!

-- Политика
create policy "Access via Org Func"
on public.documents
for select
using ( public.has_org_access(org_id) );

Пометка stable критически важна для оптимизатора Postgres.



5. Как дебажить RLS и не сойти с ума

Ты написал политику, а данных нет. Или есть лишние. Как проверить, не ломая прод?

Используй SQL Editor в дашборде Supabase или локальный psql. Мы можем притвориться любым юзером.

Симуляция запроса:

SQL
-- 1. Начинаем транзакцию, чтобы ничего не сломать
begin;

-- 2. Притворяемся конкретным юзером (берем реальный UUID из auth.users)
select set_config('request.jwt.claim.sub', 'd0a5e821-68f7-43c3-9823-xxxxxxxxxxxx', true);
select set_config('role', 'authenticated', true);

-- 3. Пробуем сделать запрос, который делает наш фронтенд
select * from public.todos;

-- 4. Смотрим результат. Если пусто — политика не работает.

-- 5. Пробуем вставить данные
insert into public.todos (title, user_id) 
values ('Test Todo', 'd0a5e821-68f7-43c3-9823-xxxxxxxxxxxx');

-- 6. Откатываем изменения
rollback;

6. Auth Settings: Что нужно включить в дашборде

Помимо SQL, есть пара галочек в UI Supabase Authentication -> Providers / Settings, которые влияют на БД:

  1. Enable Email Confirmations:

  • Dev: Выключить. Бесит подтверждать почту при каждом тесте.
  • Prod: Включить. Иначе боты заспамят базу "мертвыми" юзерами.
  1. User ID Generation:
  • Всегда используйте UUID. Supabase делает это по умолчанию. Никогда не используйте integer auto-increment для user_id в распределенных системах.
  1. Schema auth:
  • Никогда не модифицируйте таблицы в схеме auth вручную (добавлять колонки и т.д.). Supabase обновляет эту схему автоматически, и ваши изменения затрутся или сломают миграцию. Используйте public.profiles и связь по id.

7. Итоговое мнение

RLS — это мощь, но с ней легко перемудрить.

Мои золотые правила:

  1. KISS: Политика должна быть настолько простой, насколько возможно. Идеально: uid = auth.uid().

  1. Индексы: Все колонки, участвующие в RLS (например, user_id, org_id, status), обязаны быть проиндексированы. Иначе RLS превратит любой запрос в Full Table Scan.
  1. Безопасность по умолчанию: Сначала ENABLE RLS, потом пишем код.

Нужен похожий проект?

Свяжитесь с нами для бесплатной консультации.

Обсудить задачу
Postgres RLS & Auth: Архитектура безопасности | Блог ShmonovStudio