前回は、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日間隠れていた理由のふたつめを書きます。
自分でやった「良いこと」が原因でした。
