RLSのポリシーを足すほど、見える範囲が広がりました

前回は、RLS のポリシーを書いてもデータが読めなかった話でした。
権限が GRANT と RLS の二段構えになっていて、下の段だけ書いていたという内容です。

今回は、その上の段を通したあとの話になります。

管理画面を作り始めた日でした。
ログインして /dashboard を開くと、自分のカードが並ぶ。そういう画面です。

開いたら 11枚出ました。

正しくは 7枚です。
残りの4枚は、デモ用に作った別アカウントのカードでした。

目次

アプリ側では、あえて絞っていませんでした

先に書いておくと、これはアプリのコードの書き忘れではありません。

同じファイルに、自分でこうコメントしてありました。

// 🚨 .eq('user_id', ...) は書かない。RLS の auth.uid() = user_id が絞るので、
//    ここで絞ると二重になる。非公開のカードも本人には見える
const { data: items } = await client
  .from('items')
  .select('id, title, type, visible, sort_order')
  .order('sort_order')

前の工程で「アプリを信用せず DB で守る」と決めていたので、そのとおりにしました。
絞り込みは RLS に任せる。アプリ側では条件を書かない。

方針としては、いまも間違っていないと思っています。
ただ、任せた相手が仕事をしていませんでした。

ポリシーは2本とも、正しく書けていました

items の SELECT には、ポリシーを2本置いてありました。

-- 公開ページ用
create policy "visible items are viewable by everyone"
  on public.items for select using (visible = true);

-- 本人だけ
create policy "users can view own items"
  on public.items for select using (auth.uid() = user_id);

1本目は公開ページのためのものです。未ログインの人でも公開カードは読めます。
2本目は管理画面のためのものです。本人なら非公開のカードも読めます。

用途としては、きれいに分かれています。
1本ずつ読むかぎり、どちらも書きたかったとおりでした。

足したポリシーは、OR でつながっていました

PostgreSQL の公式ドキュメントに、こう書いてあります。

All permissive policies which are applicable to a given query will be
combined together using the Boolean “OR” operator.

同じ操作に対する PERMISSIVE なポリシーは、OR で結合される。
そして同じページに、こうもありました。

Policies are permissive by default.

明示しなければ PERMISSIVE です。自分が書いた2本も、当然そうでした。

ログイン中に効いていた条件は、合成するとこうなります。

(visible = true) OR (auth.uid() = user_id)

左側だけで、もう通ってしまいます。
開発中のカードは全部 visible = true にしてあったので、全行が左側で通りました。

厳しい条件を2つ書いたつもりでした。
実際には、通り道を2本用意していました。

用途では分けていたのに、ロールでは分けていませんでした

原因はもう1つの既定にありました。

The default is PUBLIC, which will apply the policy to all roles.

ポリシーには「どのロールに適用するか」を書く TO 句があります。
書かなければ PUBLIC、つまり全ロールです。

1本目の「公開カードは誰でも読める」の「誰でも」を、
自分は未ログインの人だけのつもりで書いていました。

ロールを指定していないので、ログイン済みのユーザーにも適用されます。
自分にも、他人にも、同じように効いていました。

修正はこれだけでした。

drop policy "visible items are viewable by everyone" on public.items;

-- 公開ページは cookie なし(=anon)で読むので、anon にだけ適用すれば足りる
create policy "visible items are viewable by anon"
  on public.items for select to anon using (visible = true);

to anon の2語です。
適用してリロードしたら、11枚が7枚になりました。

増やすほど緩くなる仕組みでした

整理するとこうなります。

種類 結合 増やすと
PERMISSIVE(既定) OR 通り道が増える
RESTRICTIVE AND 条件が増える

厳しくしたいなら RESTRICTIVE を使う、という選択肢もありました。
今回は用途がロールで分かれていたので、TO を書くほうが素直でした。

自分が引っかかったのは、たぶん言葉のほうです。

「ポリシー」も「制限」も「セキュリティ」も、締める側の言葉に見えます。
書いているものが増えれば、締まっていく感じがしていました。

おわりに

RLS を「行に鍵をかけるもの」だと思っていました。

実際に書いていたのは、どの条件なら通すかの列挙でした。
列挙は、増えるほど通り道が増えます。

1本ずつ読んで正しくても、合成した式が正しいとは限りません。
そして SQL のファイルには、合成した式のほうは書いてありませんでした。

次回は、この穴が4日前の検証をすり抜けていた理由を書きます。
検証は、ちゃんと通っていました。

目次