ユーザーがログインできるからといって、アプリケーション内のすべてのデータを参照・変更する権限があるとは限りません。認証はユーザーを識別し、認可は何を、どのリソースに対して、どの条件下で実行できるかを決定します。ルートで有効なセッションを要求していても、ユーザーとリソースの関係を確認しなければ、別アカウントの注文を閲覧できてしまうことがあります。
PHPの認可テストでは、WebルートやAPIへのHTTPリクエストなど、アプリケーションが利用する入口からその境界を検証します。目的はフレームワークのポリシー実装をテストすることではなく、誰が何をできるのか、いつリクエストが拒否されるのか、データが変更されずに保たれるかという、観測可能な振る舞いを確認することです。
アクセスルールをマトリクスに落とし込む

テストを書く前に、ルールをアクター、アクション、リソース、コンテキストの4要素で表します。コンテキストには、同じ組織に属すること、レコードの所有者であること、特定の状態であることなど、権限を変える条件が含まれます。この構造により、認可をロールの一覧だけに還元せずに済みます。
- アクター:未認証ユーザー、メンバー、責任者、管理者。
- アクション:参照、作成、編集、削除、承認。
- リソース:注文、ドキュメント、アカウントなどの保護対象。
- コンテキスト:所有者、組織、リソースの状態、有効な関係。
たとえば、メンバーは自分の注文を参照でき、責任者は自組織の注文を参照できるというルールが考えられます。管理者は、実際の製品ルールに従って、より広いアクセス権を持つ場合があります。マトリクスにすれば、業務上の各ルールを検証可能なケースに変換でき、抜けている組み合わせも明らかになります。
考えられるすべての組み合わせをテストする必要はありません。未認証ユーザー、同じ組織の2人のユーザー、異なる組織のユーザー、特権ロールを持つユーザー、本人に属さないリソースなど、信頼境界を優先します。アクティブな注文と権限が異なる場合のアーカイブ済み注文のように、判断を変える特殊なコンテキストも加えます。
代表的な統合テストシナリオを準備する
テストデータは明示的かつ少量に準備します。典型的なテストでは、2つの組織、それぞれに属するユーザー、いずれかの組織に紐づくリソースを作成します。次に、アクセスを試みるユーザーとして認証し、リソースの識別子を指定して、アプリケーションの公開ルートにリクエストを送ります。これにより、入口、認証、認可、レスポンスをまとめて検証できます。
重要なルールごとに、少なくとも許可されるケースと拒否されるケースを1つずつ含めます。所有者がリソースを編集できるなら、許可された編集が成功することと、別のユーザーが同じ編集を実行できないことを確認します。許可されるケースも不可欠です。拒否だけを期待するテストスイートは、権限を持つユーザーまでアプリケーションがブロックしていても通過する可能性があります。
リソースが存在しない場合も確認します。不明な識別子へのレスポンスは、存在するがアクセス権のないリソースへのレスポンスと異なる場合があります。リソースの存在を明かさないために「見つからない」レスポンスを使うアプリケーションもあれば、アクセスが禁止されていると伝えるアプリケーションもあります。テストでは、普遍的な慣例を押し付けるのではなく、製品として意図的に定めたポリシーを反映します。
有効で読みやすい状態を作れるファクトリー、フィクスチャビルダー、テストヘルパーを使います。固定の識別子、実行順序、別のテストが変更する可能性のある共有データへの依存は避けます。検証対象に永続化が含まれる場合は、プロジェクトで通常使うツールを使ってデータベース上の結果を確認します。成功レスポンスだけで正しい状態が保証されると思い込まないでください。
ユーザー間・組織間の分離をテストする
URLやリクエスト本文の識別子を差し替えたときに不具合が起きやすいため、分離については専用のケースを用意します。あるアカウントのリソースを作成し、別のユーザーとして参照、編集、削除を試みます。企業ごとのデータスコープがある場合は、組織をまたいだ検証も繰り返します。重要な操作では、アクションごとにテストしてください。読み取りが保護されていても、ダウンロード、エクスポート、更新まで保護されているとは限りません。
テストスイート全体の重複を避けるため、共通の準備処理を共有し、ルールを表す軸だけをパラメーター化します。たとえば、アクターとリソースの組み合わせのリストで、許可される組み合わせを定義できます。ケース名は具体的に保ちます。「別組織のメンバーは注文を編集できない」は、不透明な真偽値の集合よりも多くを説明します。メソッド、ルート、期待する効果が異なる場合は、シナリオを分けます。
異なるルールを表さない組み合わせが多い場合、すべてのロールとすべてのリソースを組み合わせてテストするのは避けます。代わりに、妥当な根拠のある同等性を特定し、例外や境界については明示的なケースを残します。ロールに階層がある場合でも、あるロールが別のロールの権限を自動的にすべて継承すると決めつけず、製品が定める実際のルールを検証します。
レスポンスと副作用を検証する
リクエストが拒否された場合は、レスポンスだけでなく、保護対象の操作が実行されていないことも確認します。インターフェースに応じて、レスポンスはリダイレクト、未認証または禁止を示すステータス、またはリソースが見つからないことを示すレスポンスになります。不要な表示上の詳細にテストを結び付けず、関連する契約(ステータス、形式、必要な場合はメッセージ)を確認します。
更新が拒否された場合は、機密フィールドが変わっていないことを確認します。削除の場合は、レコードが引き続き存在することを確認します。請求書、通知、イベントを生成する操作なら、それらの副作用も発生していないことを検証します。アサーションでは、HTTPステータスだけでなく、ビジネス上重要な効果を確認します。
未認可のリクエストによって、部分的な変更が発生してもいけません。処理に複数の操作が含まれる場合は、変更前に拒否されること、またはトランザクションによってシステムが整合性のある状態に保たれることを検証します。これを証明するためにprivateメソッドを調べる必要はありません。レスポンスと、永続化されたデータや副作用を観察します。
実装変更に強いテストを維持する
統合テストでは、認証済みユーザーのリクエストやルートなど、アプリケーションの安定したインターフェースを経由します。実際のリソースアクセスを検証したいのであれば、内部のポリシークラスを直接呼び出すのは避けます。そうすると、ルート登録、ミドルウェア、オブジェクトの読み込み方法がテスト対象から漏れる可能性があります。
一方で、テストスイートをアプリケーション全体の複製にしてはいけません。代表的な入口で認可の契約をテストし、多くのコンテキストを要する純粋なルールにはユニットテストを使います。この組み合わせにより、問題の切り分けが容易になります。ルールのテストでは条件を個別に検証でき、統合テストではそのルールが公開されたフローを保護していることを確認できます。
ロール、ルート、ポリシーが変更されたら、期待値を変更する前にマトリクスを見直します。新しい結果を受け入れるためだけにテストを更新すると、意図しない権限拡大を見逃す可能性があります。ルールが変更された理由を記録し、アクセス権を得るアクターと失うアクターを特定したうえで、新しい境界を守るケースを追加します。
変更をレビューするためのチェックリスト

- アクター、アクション、リソース、コンテキストが定義されていますか?
- 影響を受けるルールについて、許可されるシナリオと拒否されるシナリオが少なくとも1つずつありますか?
- 該当する場合、ユーザー間またはデータスコープ間のアクセスがテストされていますか?
- リソースが存在しない場合と、情報を開示しないために選んだポリシーがカバーされていますか?
- リクエストは、保護すべき入口を通っていますか?
- 拒否によってデータが変更されたり、副作用が生じたりしないことを確認していますか?
- テストデータは分離され、読みやすく、再現可能ですか?
- 期待値は製品としての決定を反映しており、フレームワークの偶発的な仕様に依存していませんか?
有用なテストスイートが抽象的に「権限がある」ことを示すのではありません。重要な組み合わせが機能し、未認可の組み合わせが境界を越えないことを示します。このマトリクスを具体的な統合テストとともに維持すれば、セキュリティを特定の内部実装に結び付けずに、アクセス変更をレビューしやすくなります。



