RLSポリシーを7本書いても、データは1行も読めませんでした

自作アプリ「Hubpin」を作っています。
分散した自分の発信を1ページに集めるハブサイトで、Next.js 16 と Supabase の組み合わせです。

この日はデータベースの設計をしていました。
テーブルを2つ作って、行レベルセキュリティ(RLS)のポリシーを7本書いたところです。

「公開カードは誰でも読める」「自分のカードは本人が読める」「自分のカードは本人が消せる」。
そういう条件を、1本ずつ SQL で書いていく作業でした。

書き終わったので select を叩いてみました。

permission denied for table items

1行も返ってきません。

ポリシーは何度読み返しても間違っていませんでした。

目次

権限は二段構えになっていました

PostgreSQL の公式ドキュメントに、そのままの説明がありました。

In addition to the SQL-standard privilege system available through GRANT,
tables can have row security policies that restrict, on a per-user basis,
which rows can be returned by normal queries

「GRANT による権限の仕組みに加えて」と書いてあります。
つまり RLS はその上に乗るものであって、置き換えるものではありませんでした。

何を決めるか
GRANT そのテーブルに触れてよいか
RLS 触れるとして、どの行か

自分が7本書いたのは、下の段だけでした。

上の段が無いので、下の段には到達していません。
どの行を見せるかは丁寧に決めていたのに、テーブルを開ける鍵のほうを持っていませんでした。

「既定で付く」と書いてあるものが、付いていませんでした

Supabase 側のドキュメントも読みました。

By default, tables in the public schema are granted SELECT, INSERT,
UPDATE, and DELETE to the anon and authenticated roles.

既定で付く、と書いてあります。
付くはずのものが、手元では付いていませんでした。

なぜ付かなかったのかは、いまも特定できていません。
マイグレーション経由でテーブルを作ったことと関係がありそうに見えますが、
確かめていないことを原因として書くのはやめておきます。

ひとつ引っかかったのは、この一文が載っていた場所です。
「権限がありません」というエラーのトラブルシューティングページでした。

既定で付くはずのものが付かない場面は、それなりにあるということだと受け取りました。

エラーが直し方を教えてくれました

PostgreSQL は親切です。エラーと一緒に、こうすればいいという提案が出てきました。

HINT: GRANT INSERT ON public.items TO anon;

貼り付ければ動きます。
そして「未ログインの誰でもカードを追加できるサイト」ができあがります。

anon は未ログインの人に割り当てられるロールです。
そこに INSERT を渡すのは、鍵をかけないまま投稿フォームを置くのと同じでした。

HINT が答えているのは「どうすれば動くか」です。「そうしていいか」ではありません。

ここは、AI に聞いたコードをそのまま貼るときの事故と同じ形をしていると思いました。
返ってきたものは、たしかに動きます。動くことしか保証していません。

最小限だけ渡すことにしました

マイグレーションを1本足しました。既存のファイルは書き換えていません。

-- 未認証(anon)は読むだけ。書き込み権限そのものを与えない
grant select on public.profiles to anon;
grant select on public.items    to anon;

-- ログイン済み(authenticated)は操作できる。
-- 🚨 ただし「どの行を」操作できるかは RLS ポリシーが決める
grant select, update on public.profiles to authenticated;
grant select, insert, update, delete on public.items to authenticated;

Supabase の標準的なやり方は「全部 GRANT して、あとは RLS で制御する」でした。
これは採りませんでした。

未ログインの人がカードを書き込む理由がひとつも無いなら、
RLS で弾く前に、権限そのものを渡さなければいい。

防御が1枚から2枚になります。
このとき自分は、それを素直に良いことだと思っていました。

おわりに

この工程には他にもいくつか罠がありましたが、
手順書に書いてあったものは全部よけられました。止まったのはここだけです。

予見できていた罠は回避できて、予見できなかった罠だけが時間を使いました。

そして分かったのは、「RLS を書いた」は「権限を設計した」ではないということでした。
ポリシーの一覧をいくら眺めても、その手前で誰がテーブルに触れるのかは書いてありません。
別のファイルに、別の文法で置いてあります。

次回は、そのポリシーのほうに穴が開いていた話を書きます。

目次