全組み合わせで安全を証明する ― 権限判定を高速化した時のテスト設計
目次 [非表示]
権限の判定を速くするとき、私たちは「洗い出した条件の範囲では、答えが1つも変わらないこと」をテストコードで機械的に確かめてから出すことにしました。判定に効く条件をすべて掛け合わせた384通りを全件確かめ、さらにテストそのものを13種類の「わざと壊した実装」で試しています。この記事では、その設計と、リリース前にテストの網の目の穴まで潰した経緯を紹介します。
この記事で分かること
- 高速化しても答えを変えないための「全組み合わせテスト」の作り方
- テストの見落としを見つける「わざと壊す」確かめ方
- 障害時に必ず「拒否」側へ倒す設計と、少しずつ有効にする公開方法
- 人が基準を決め、AIに実装と検証の手間を担わせた役割分担
※記事中のコード例は、説明のために簡略化した架空のものです。
なぜ権限判定を速くしたかったのか?
ピジョンクラウドは、800以上の企業が使うマルチテナントのWebデータベースです。テーブル、レコード、項目ごとに「誰が見られるか」「誰が編集できるか」を細かく設定できます。
| 単位 | 設定できること(例) |
|---|---|
| テーブル | 営業部だけが閲覧できる、管理者だけが編集できる |
| レコード | 担当者本人と所属部署だけが見られる |
| 項目 | 原価の欄は特定のグループにだけ表示する |
この判定は、ほぼすべてのリクエストで何度も走ります。そこで、1回のリクエストの最初に「このユーザーがどのグループ・部署に属しているか」をまとめて読み込み、その結果を使い回す方式に変えました。私たちはこれを「スナップショット方式」と呼んでいます。
// リクエストの最初に、所属情報を1回だけ読み込む
$membership = MembershipSnapshot::loadFor($currentUser);
foreach ($records as $record) {
// 以降の判定は、読み込み済みの所属情報だけで行う
$canView = $accessRule->canView($record, $membership);
}
「数社で試して一致した」は証明になるのか?
権限は、1か所でも判定がずれると、見えてはいけないデータが見えてしまいます。速くなっても、答えが1つでも変わったら意味がありません。
"たぶん大丈夫"で出していいのか?
仮に実データの数社で新旧の結果が一致しても、それは安心材料にはなりますが、その会社に「たまたま無かった設定の組み合わせ」は確かめられていません。
そこで今回は、サンプルでの確認を主な根拠にしない、と決めました。代わりに、判定に効く条件をすべて洗い出し、組み合わせを全部テストコードで確かめる方針をとっています。
| 方法 | 分かること | 分からないこと |
|---|---|---|
| 実データでの突き合わせ | その会社の設定では一致する | 試していない組み合わせの結果 |
| 全組み合わせテスト | 洗い出した条件のすべての組み合わせで一致する | 条件の洗い出し自体の漏れ(→次の章で確かめる) |
384通りの全組み合わせテストはどう作ったのか?
まず、判定の結果を左右する条件を1つずつ書き出しました。条件ごとの取りうる値を掛け合わせると、ちょうど384通りになります。今回の検証では、この384通りを全件確かめました。
| 条件 | 通り | 累計 |
|---|---|---|
| 「全員に許可」の設定 | 4 | 4 |
| グループにメンバーがいるか | 2 | 8 |
| グループに部署が入っているか | 2 | 16 |
| ユーザーを直接指定しているか | 2 | 32 |
| 部署を直接指定しているか | 2 | 64 |
| 権限の種類 | 3 | 192 |
| 厳密モード | 2 | 384 |
次に、高速化した実装とは別に、「この条件ならこうなるべき」を素直に書いた正解の定義(参照実装。テストオラクルとも呼ばれます)を用意しました。速さは気にせず、仕様をそのまま読める形で書いたものです。384通りすべてで、実装の答えと正解の定義の答えを突き合わせます。
// 384通りをすべて生成し、1件ずつ突き合わせる(架空の簡略化コード)
foreach (allCombinations() as $case) {
$expected = referenceRule($case); // 仕様を素直に書いた「正解の定義」
$actual = fastRule($case); // 高速化した実装
assertSame($expected, $actual, describe($case));
}
テストが正しいことは、どう確かめたのか?
全件が一致しても、まだ安心はできません。正解の定義を置くだけでは、テストが実装の誤りを本当に見分けられるかまでは分からないからです。そこで、テストが代表的な誤りを検知できるかを確かめることにしました。
わざと壊した実装で、テストの網を試す
- 実装の条件を1つだけわざと壊した版を作る(例:「かつ」を「または」に変える、条件を1つ消す)
- その壊れた版に、384通りのテストを流す
- どこかの組み合わせで必ず失敗すれば「このテストは、その壊れ方を見逃さない」と言える
この考え方は、一般にミューテーションテストと呼ばれます。下の表は理解のための仮想の例で、実際に作った13種類の内訳ではありません。
| 壊し方の例(説明用) | 384通りのテスト |
|---|---|
| 「全員に許可」の判定を常に許可にする | 失敗する=検知 |
| 部署の直接指定を見ないようにする | 失敗する=検知 |
| 厳密モードの分岐を逆にする | 失敗する=検知 |
実際の検証では13種類の壊れた版を用意し、13種類すべてをテストが検知することを確認しました。
リリース前に見つけた「網の目の穴」とは?
13種類の確認に加えて、実装を1行ずつ壊して同じことを試しました。すると、1か所だけ、壊してもテストが通ってしまう行が見つかりました。
見つかった穴
メンバーが個人ではなく部署だけで登録されているグループを、「誰もいない空のグループ」と取り違えうる条件です。この条件の誤りに対して、テストの検知力が足りていないことが分かりました。
本番に出る前の段階だったので、すぐにこの誤りを検知するテストを追加しました。そのうえで、13種類と1行ずつの壊し方のすべてを検知できる状態になったことを確かめてから、リリースに進んでいます。
// 追加したテストのイメージ(架空の簡略化コード)
public function test部署だけのグループ(): void
{
// 個人のメンバーは0人、部署だけが所属しているグループ
$group = makeGroup(users: [], departments: [$salesDept]);
$user = makeUser(department: $salesDept);
// 部署を通じた所属として判定され、正解の定義と一致すること
$case = caseFor($user, grantedTo: $group);
assertSame(referenceRule($case), fastRule($case));
}
発見の時点
リリース前
対応
検知するテストを追加
結果
すべての壊れ方を検知
テストが全部通ることと、テストが十分であることは別の話です。わざと壊してみることで、その差を事前に埋められました。
速さより先に決めていたことは何か?
高速化の設計では、速さより先に「失敗したときにどちらへ倒れるか」と「どう出していくか」を決めていました。
| 決めごと | 内容 |
|---|---|
| 迷ったら拒否 | 所属情報の読み込みに失敗したときは、権限を緩めるのではなく、必ず「見せない」側に倒す。この動きもテストで固定した |
| 少しずつ有効にする | 高速化は切り替え式にし、全社一斉ではなく企業ごとに順番に有効にできるようにした。初期値は無効=何もしなければ従来と同じ動き |
最終的に、関連するテスト458件・922アサーション(検証条件)がすべて通過しました。レビューの指摘もすべて解消してから出しています。
人とAIは、どう役割を分担したのか?
384通りの突き合わせや、13種類の壊れた版での確認は、毎回人の手だけで回すには手間のかかる作業です。そこで今回は、安全の基準は人が決め、実装と検証の手間はAIに担わせるという分担で進めました。AIには、統括・実装・レビューの3つの役を分けて持たせています。
| 担当 | 担ったこと |
|---|---|
| 人 開発責任者 | 「数社の実データで一致した、を安全の根拠にしない。設計の正しさはテストコードで担保する」という基準を決めた。リリースしてよいかの最終判断も人が行った |
| 統括役のAI Claude Code | 要件を整理して作業を分け、実装役に委託した。戻ってきた結果をそのまま受け取らず、自分でテストを流し直し、実装を1行ずつわざと壊して検知できるかを独立に確かめた。その確認で、テストをすり抜ける1か所を見つけた |
| 実装役のAI OpenAIのCodex | 高速化の実装、384通りの全組み合わせテスト、13種類の壊れた版での検証、見つかった穴を塞ぐテストを書いた |
| レビュー役のAI OpenAIのCodex | 実装した会話とは切り離した別の会話・別の文脈で設計をレビューした。重大な指摘4件のうち1件が、新旧の答えを網羅テストで担保する必要性だった |
実装とは別の会話で行った設計レビューでは、重大な指摘が4件出ました。その1件が「新旧で答えが変わらないことを、網羅的なテストで担保すべき」というものです。ここから、384通りの全組み合わせテストと、テストを疑う13種類の壊れた版での検証が生まれました。
検証が組み上がった流れ
人安全の基準を決める
統括役要件を整理して作業を分け、実装役に委託する
実装役高速化の実装とテストを書く
レビュー役実装とは別の会話で設計を疑い、網羅テストの必要性を指摘する
実装役384通りの全組み合わせテストと、13種類の壊れた版での検証を加える
統括役テストを流し直し、実装を1行ずつ壊して試す。すり抜ける1か所をリリース前に見つける
実装役見つかった穴を塞ぐテストを追加する
人結果を見て、出してよいかを判断する
運用の工夫:書いた本人(同じ会話)にレビューさせない
同じ会話の続きでレビューを頼むと、自分の書いたものを弁護する方向に働きやすくなります。そのため、レビューは実装とは切り離した別の会話で読ませ、さらに統括役のAIが実装を自分で壊して確かめる、という二重のチェックにしました。
人だけでは手が回らない量の検証を、実装とは別の会話での設計レビューと、統括役による再実行・破壊確認を重ねることで確保しています。
ほかの改修でも同じ考え方は使えるのか?
使えます。別の、権限の「所有者」判定に関わる変更でも、同じ検証の型を使いました。ポイントは、変更前のコードの結果を「正解の記録」として残しておくことです。
変更前後を突き合わせる手順
- 変更前のコードで、全74通りの判定結果を保存する
- 変更後のコードで同じ74通りを評価し、全件が完全に一致することを確かめる
- 新しく書いたテストを変更前のコードにも流し、意図したテストだけが失敗することを確かめる
3番目は、テストが変更点を本当に検出できるかを見る確認です。変更前のコードで失敗しないテストは、変更の効果を確かめられていません。
よくある質問
Q. 全組み合わせのテストは、条件が増えると現実的ではないのでは?
条件の数と取りうる値が限られている部分なら、今回の384通りのように全件を確かめられます。組み合わせが爆発する場合は、同値クラスや境界値で入力を整理し、ペアワイズ法やプロパティベーステストを併用する方法があります。処理を小さく分けて各部分を網羅しつつ、部分同士の主要な組み合わせは結合テストで確かめます。
Q. 「正解の定義」を別に書くのは二度手間では?
二度手間ですが、そこに価値があります。速さを気にせず仕様どおりに書いたコードと突き合わせることで、高速化のための工夫が答えを変えていないことを確かめられます。
Q. ミューテーションテストはすべてのコードに必要ですか?
すべてのコードに一律で必要なものではありません。今回の権限判定のように、1か所の誤りが大きな影響につながる部分で特に有効な手法です。
Q. 障害時に「拒否」側へ倒すと、使えなくなる人が出ませんか?
一時的に見えなくなる可能性はあります。それでも、見えてはいけないデータが見えてしまうよりは安全だと判断しています。
Q. AIに実装を任せて、品質は大丈夫ですか?
任せているのは実装と検証の手間で、品質の基準ではありません。何を安全とみなすかは人が決め、AIが書いた実装は網羅的なテストで確かめています。設計は実装とは別の会話のAIにレビューさせ、さらに統括役のAIが実装をわざと壊して検知できるかを確かめたうえで、出してよいかを人が判断しています。
Q. ピジョンクラウドではどんな権限設定ができますか?
テーブル・レコード・項目の単位で、閲覧や編集をユーザー・部署・グループごとに設定できます。詳しくは機能一覧とセキュリティへの取り組みをご覧ください。
まとめ:テストで両立させる速さと安全
「速さ」と「安全」はトレードオフではなく、網羅的なテストと段階的な公開によって両立できます。今回は、条件を洗い出して全組み合わせを確かめ、テストそのものをわざと壊した実装で試し、リリース前に網の目の穴まで潰しました。基準と最終判断は人が持ち、手間のかかる実装と検証はAIに担わせることで、この徹底を毎回続けられる形にしています。
今回の検証の数字
- 全組み合わせテスト:384通り
- わざと壊した実装:13種類をすべて検知
- 関連テスト:458件・922アサーションがすべて通過
お客様の大切なデータを預かるSaaSとして、ピジョンクラウドはこうした見えにくい部分に手間をかけています。権限設定を細かく作り込みたい方は、30日間の無料トライアルで実際の設定画面をお試しください。
データ管理、もっと簡単に。
Excel・Access・スプレッドシートの課題を、ノーコードのWebデータベース「PigeonCloud」が解決します。
月額5,500円〜(5ユーザー)、30日間無料でお試しいただけます。