権限の検証が通ったのは、そのとき他人がいなかったからでした

前回は、RLS のポリシーが OR で結合されて、他人のカードまで見えていた話でした。
今回は、そのバグが4日間見つからなかった理由のうち、ひとつめを書きます。

先に言っておくと、検証はしてありました。
しかも、通っていました。

目次

検証SQLは書いてありました

データベースを設計した工程で、動作確認用の SQL を1ファイル用意していました。

ロールを切り替えて、期待どおりの結果が返るかを見るものです。

  • 未ログインだと公開カードだけ見える
  • 他人のカードは書き換えられない(0行が返る)
  • 本人なら書き換えられる
  • 非公開のカードは未ログインから見えないが、本人には見える

書いた当時、全部そのとおりになりました。
それで「権限まわりは終わった」と判断して、次の工程へ進んでいます。

4日後に、同じテーブルで穴が見つかりました

管理画面を作り始めたら、他人のカードが一覧に出てきました。
ポリシーは、あの日から一度も変えていません。

では、なぜ検証を通ったのか。

Table Editor で created_at を見たら分かりました。

ユーザー カード 作成日時
自分 7件 2026-08-06 02:49(検証した工程)
デモ用アカウント 4件 2026-08-07 22:44(その翌日の工程)

検証したとき、テーブルには他人の行が1件もありませんでした。

自分のデータしか入っていないテーブルでは、「自分の行だけ返すポリシー」と「全部返すポリシー」は同じ結果を返します。

区別がつく材料が、その日はまだ存在していませんでした。

「他人」はいました。「他人のデータ」が無かっただけです

ここが少し気持ち悪いところです。

ユーザー自体は検証した日にはもう2人いて、プロフィールのテーブルには2行入っていました。

入っていなかったのは、その2人目のカードのほうです。

自分の感覚では「デモ用のユーザーはもう作ってある」だったので、
他人がいない状態でテストしている、という自覚がありませんでした。

テーブルごとに「他人が存在するか」は別々に進みます。
片方で揃っていても、もう片方では揃っていない。

再検証すべき日は、別のところにありました

デモ用のカードを入れたのは、検証の翌日でした。

他人のデータが生まれたのは、その瞬間です。
つまりその日が、権限まわりを確かめ直す日でした。

でも自分の中ではその工程は「ログイン機能を作る工程」でしかなく、デモ用のデータを入れたのもログイン画面を試すための準備でした。

権限の検証は、前の工程で終わったことになっていました。

検証SQLはファイルに残っていて、いつでも実行できます。
書いてあったのに、データが変わったあとに走らせていませんでした。

一度もです。

期待値の書き方を変えました

反省としてはここに落ちました。

件数で書く 「7件返ること」。データが増えた瞬間に嘘になる
不変量で書く 「返ってきた行に、自分以外の user_id が0件であること」

件数はデータの状態に依存します。不変量は依存しません。

追加した検証はこうなりました。

-- 🚨 期待値を「件数」で書かない。データが増えると陳腐化する(それが今回の見落とし)
select count(*) filter (where user_id <> 'a12690b6-…') as 他人の行数
from public.items;   -- 期待: 0

カードが何枚に増えても、この式は正しいままです。

ついでにファイルの冒頭にも、この書き方には寿命がある、と書き足しました。
個別に直しても、次に書くときに同じ形で書いてしまうからです。

おわりに

権限の検証は、他人のデータが存在して初めて意味を持ちます。

自分ひとりしかいないテーブルで通した検証は、「通った」というより
「まだ試していない」に近い状態でした。
結果は緑でしたが、確かめたいことは何も確かめていません。

テストの前提条件は書いたときのまま止まってくれず、データが増え、ユーザーが増え、条件のほうが静かに変わっていきます。

チェック項目は残るのに、それが何を証明していたかは残らない。
そこが厄介でした。

次回は、このバグが4日間隠れていた理由のふたつめを書きます。
自分でやった「良いこと」が原因でした。

目次