Skip to main content

Грабли переезда

Собрали по разборам инцидентов. Каждый пункт стоил кому-то от десяти минут до получаса, и ни один не виден из кода.

1. Роль в миграции — только через layero.role()

Имя роли содержит идентификатор базы, поэтому в миграцию его не впишешь. Найти по шаблону кажется естественным:

-- 🚨 ТАК НЕЛЬЗЯ
SELECT rolname FROM pg_roles WHERE rolname ~ '^u_.*_anon$' LIMIT 1;

Список ролей общий на весь сервер баз. Шаблон совпадёт с ролями чужих арендаторов, и GRANT уедет в чужую базу: однажды так права на таблицы одного продукта достались роли чужой организации, и заметили это только потому, что свои же запросы продолжали отвечать «таблица не открыта».

-- Так правильно
GRANT SELECT ON public.entries TO layero.role('anon');

Функция layero.role('anon'|'auth'|'svc') есть в каждой базе с включённым API. Проверить выданное можно так:

SELECT layero.role('anon'), has_table_privilege(layero.role('anon'), 'public.entries', 'SELECT');

2. INSERT … RETURNING проходит политику чтения

supabase-js по умолчанию делает .insert().select(), то есть просит вернуть вставленную строку. Возврат — это чтение, и он идёт через политику SELECT.

Классический случай: участника рабочего пространства добавляет триггер AFTER INSERT, но право прочитать возвращаемую строку RLS проверяет до того, как триггер отработает. Итог — new row violates row-level security policy, хотя та же вставка без возврата проходит. Ошибка звучит как проблема со вставкой, а причина в чтении.

Лечится политикой чтения, которая пускает и автора:

CREATE POLICY workspaces_read ON public.workspaces
FOR SELECT TO PUBLIC
USING (owner_id = auth.uid() OR EXISTS (
SELECT 1 FROM public.members m
WHERE m.workspace_id = id AND m.user_id = auth.uid()));

Либо вставкой без возврата: db.from("workspaces").insert(row) без .select().

3. Функции живут только в схеме api

Таблицы шлюз ищет в api, public и app, функции — только в api. Accept-Profile на это не влияет. Подробности — Функции и RPC.

4. Имя аргумента функции — публичный контракт

Ключи JSON раскладываются по одноимённым аргументам, поэтому имя аргумента видно снаружи и попадает в код каждого клиента. Внутри функции оно же конфликтует с одноимённой колонкой (column reference "token" is ambiguous), и локальная переменная не спасает — она подставляется и в ON CONFLICT. Рецепт — там же, в Функции и RPC.

5. Пустой ответ — это работающая политика

[] в ответе значит «грант есть, строк по вашей политике нет». Это не ошибка и не поломка доступа. Отличать полезно так:

ОтветПричина
unknown_tableтаблицы нет в каталоге вашей роли — почти всегда грант ушёл не той роли
no_table_grantтаблица есть, права нет; готовый GRANT лежит в hint
[]права есть, политика не пускает строки

Проверьте в панели, что политика держит чужого: запрос выполнится от роли ключа, а транзакция откатится.