Laravel Policyで認可を正しく実装する

はじめに

APIエンドポイント /api/projects/{id} において、認証(Authentication)は通っているものの、認可(Authorization)の検証が不十分なケースがあります。

例えば、ユーザーAが自身のトークンを用いて、ユーザーBが所有する project_id を指定して GET / PUT / DELETE リクエストを送信した場合、サーバー側で所有権をチェックしていなければ、他人のデータを閲覧・改ざんできてしまいます。

これは典型的な IDOR (Insecure Direct Object Reference) 脆弱性です。

// 脆弱な例
public function update(Request $request, Project $project)
{
    // ログインしていることは確認されているが、
    // $project がログインユーザーのものかは見ていない
    $project->update($request->all());
}

Policyとは

Laravel Policyは、特定のモデルに対する認可ロジックをひとつのクラスに集約する仕組みです。

コントローラーやミドルウェアに if ($project->user_id !== auth()->id()) のようなチェックを散在させる代わりに、Policyクラスに認可ルールを定義し、コントローラーから呼び出します。

これにより以下のメリットが得られます。

  • 認可ロジックの一元管理 ─ 変更時にコントローラーを探し回る必要がありません
  • テストの容易さ ─ Policyクラス単体でユニットテストが書けます
  • 一貫性 ─ authorize メソッドやミドルウェアを通じて、どのエンドポイントでも同じルールが適用されます

Policyで解決する

1. Policyの生成

php artisan make:policy ProjectPolicy --model=Project

2. 認可ロジックの定義

app/Policies/ProjectPolicy.php に各アクションの認可ルールを記述します。

<?php

namespace App\Policies;

use App\Models\Project;
use App\Models\User;

class ProjectPolicy
{
    /**
     * プロジェクトの閲覧権限
     */
    public function view(User $user, Project $project): bool
    {
        return $user->id === $project->user_id;
    }

    /**
     * プロジェクトの更新権限
     */
    public function update(User $user, Project $project): bool
    {
        return $user->id === $project->user_id;
    }

    /**
     * プロジェクトの削除権限
     */
    public function delete(User $user, Project $project): bool
    {
        return $user->id === $project->user_id;
    }
}

ロジックはシンプルで、プロジェクトの user_id とログインユーザーの id が一致するかどうかだけを返します。

false が返れば、Laravelは自動的に 403 Forbidden レスポンスを返します。

3. Policyの登録

Laravel 8以降では、Policyの自動検出が有効です。
App\Models\Project に対して App\Policies\ProjectPolicy という命名規則に従っていれば、手動登録は不要です。

Laravel 10以前では AuthServiceProvider で明示的に登録することも可能です。
なお、Laravel 11以降では AuthServiceProvider がデフォルトのスケルトンから削除されているため、命名規則に従わない場合は AppServiceProviderboot メソッド内で Gate::policy() を使って登録してください。

// Laravel 10以前: AuthServiceProvider
protected $policies = [
    Project::class => ProjectPolicy::class,
];

// Laravel 11以降: AppServiceProvider(命名規則に従わない場合)
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::policy(Project::class, ProjectPolicy::class);
}

4. コントローラーでの適用

コントローラーの各メソッドで $this->authorize() を呼ぶだけで適用できます。

class ProjectController extends Controller
{
    public function show(Project $project)
    {
        $this->authorize('view', $project);

        return ProjectResource::make($project);
    }

    public function update(Request $request, Project $project)
    {
        $this->authorize('update', $project);

        $project->update($request->validated());

        return ProjectResource::make($project);
    }

    public function destroy(Project $project)
    {
        $this->authorize('delete', $project);

        $project->delete();

        return response()->noContent();
    }
}

これで、ユーザーAがユーザーBのプロジェクトIDを指定した場合、403 Forbidden が返却されます。

補足: authorizeResourceを使う方法

リソースコントローラーであれば、コンストラクタで一括適用もできます。

class ProjectController extends Controller
{
    public function __construct()
    {
        $this->authorizeResource(Project::class, 'project');
    }

    // 各メソッドからauthorize()の呼び出しを削除できます
}

authorizeResource は、コントローラーのメソッド名とPolicyのメソッド名を自動的にマッピングします(showviewupdateupdate など)。

まとめ

認証(Authentication)はリクエストの送信元が「誰か」を特定するだけであり、「そのリソースへのアクセス権があるか」を判定するのは認可(Authorization)の役割です。
この区別を見落とすと、IDORのような脆弱性が生まれます。

Laravel Policyを使えば、モデルごとの認可ルールを一箇所に集約でき、コントローラーでは $this->authorize() を一行追加するだけで済みます。
まだ導入していない場合は、既存のエンドポイントを一度洗い出して、Policyが適用されているか確認してみてください。

Gemini APIの429リソース不足エラーの回避策

「第3回生成AI Innovation Awards 最優秀賞」受賞プロジェクトで、Gemini APIの429リソース不足エラーへの対処としてExponential Backoff戦略が紹介されていました。

参考

背景

Gemini APIを大規模に運用すると、「429 Resource Exhausted(リソース不足)」エラーは避けられません。Vertex AIでは「スロット購入」という解決策が用意されていますが、ピーク時のアクセス量に合わせて契約すると運用コストが大幅に膨らみます。

今回のプロジェクトでは、「即時レスポンスは必須ではない」というビジネス特性を活かし、コストを抑えつつ処理を確実に完了させるリトライ戦略を採用しています。

Exponential Backoff

リトライ戦略の核となる「Exponential Backoff」は、API呼び出し失敗時に即座に再試行せず、リトライ間隔を指数関数的に延ばしていく手法です。

  • 1回目のリトライ:1秒後
  • 2回目のリトライ:6秒後
  • 3回目のリトライ:30秒後

待ち時間を段階的に増やすことで、サーバー負荷が下がるタイミングを捉えます。無計画な連続リトライによる負荷増大を防ぎながら、最終的にリクエストを成功させる合理的な設計です。

導入時の検討ポイント

この手法はコスト効率に優れる一方、適した用途の見極めが必要です。

  • 処理期限の猶予:完了まで数分〜数十分の遅延が許容されるか
  • リトライ設定の妥当性:最大待ち時間がビジネス要件を逸脱しないか
  • ユーザー体験への影響:バッチや非同期処理として切り離せているか

まとめ

追加コストを抑えながら膨大なリクエストを安定処理できるこの手法は、ビジネス要件によっては最良の選択肢となります。

RAGで類似検索がうまくいかない理由とその解決策

以下の動画の内容を基に、RAGにおける検索精度の課題と具体的な改善策についてまとめました。

要点:なぜ従来の類似検索では不十分なのか

RAGで一般的に使われる「類似検索」には、実は大きな落とし穴があります。それは、「問い」と「答え」は必ずしも言葉の意味が似ているわけではないという点です。このギャップにより、検索品質が低下するという根本的な課題を抱えています。

Googleをはじめとする高度な検索システムでは、単純な言葉の類似度ではなく、クエリ(質問)とドキュメントの「関連性(Relevancy)」を学習した「Two-Towerモデル」などの推薦モデルを採用しています。

Vertex AI Embedding APIを活用すれば、自前で膨大な学習コストをかけることなく、この高度な関連性に基づくエンベディングをすぐに実務へ取り入れることが可能です。

実務レベルの検索に求められるもの

多くのRAG入門編で推奨される「ベクトルによる類似検索」ですが、実はプロが手掛ける検索システムの現場では、そのまま使われることは多くありません。

その理由は、「空はなぜ青い?」(クエリ)という問いに対し、正解となるドキュメントには「空気の散乱(レイリー散乱)が原因で……」と書かれているからです。この両者は、使われている単語も見た目も似ていません。

ビッグテック各社は、この「問いと答えの非対称性」を解決するため、単なる「似ているかどうか」ではなく、「この質問にはこの回答が最適である」という関連性を学習させるアプローチをとっています。

検索精度を阻む「意味的ギャップ(Semantic Gap)」の正体

RAGにおける検索精度の伸び悩みは、「Semantic Gap(意味的ギャップ)」という言葉で説明されます。

従来の類似検索が抱える限界

従来のエンベディングモデル(例:sentence-transformers)の多くは、テキスト同士がどれだけ意味的に近いかを測定するように学習されています。つまり「この2つの文章は似ているか?」を判定するモデルです。

しかし、Q&A(質疑応答)のシナリオでは、以下のように「似ているもの」が必ずしも「正解」とは限りません。

クエリ(質問) 期待する正解 類似検索で上位に来がちなもの(誤答例)
空はなぜ青い? 光の散乱(レイリー散乱)の解説 「空」「青い」という単語を含むだけの中身のない文
Pythonでリストをソートするには? list.sort()sorted() の具体的な使い方 「Pythonのリストとは何か」という概念的な説明
エラー: ModuleNotFoundError 該当モジュールのインストール手順 同じエラー名が記載されているだけの別文脈のログ

ズレが生じる仕組み

類似検索モデルは、あくまで「2つのテキストの距離」を測ります。対してQ&Aでは、「質問」という入力に対して「回答」という異なる性質の情報を結びつける必要があり、これは根本的に異なるタスクなのです。

【比較】類似検索と関連性に基づく検索の違い

1. 従来の類似検索(Similar Search)

クエリ「空はなぜ青い?」に対し、ベクトル空間上で物理的に近い点を探します。
ここでは、「空が青い理由を説明します」といった、タイトルだけが似ていて中身の薄いドキュメントが優先されやすく、実際に「散乱現象」を詳しく解説している正解ドキュメントが遠ざけられてしまう傾向があります。

2. 関連性に基づく検索(Relevancy Search)

Vertex AIなどが採用している推薦モデルでは、クエリとドキュメントをそれぞれ独立して処理しつつ、「関連性が高いペア」が近くなるようにあらかじめ学習されています。
これにより、たとえ表面上の単語が異なっていても、内容として最適なドキュメントを正確に引き当てることが可能になります。

関連性を導き出す「Two-Towerアーキテクチャ」

推薦モデルの代表的な構造である「Two-Tower(双塔)モデル」について解説します。

モデルの構造

このモデルは、クエリ専用の「Query Tower」とドキュメント専用の「Document Tower」という2つの独立したエンコーダーで構成されています。それぞれの入り口から入ったデータが最終的にベクトル化され、その関連性(スコア)が算出されます。

学習のプロセス

Two-Towerモデルは、実際の検索ログ(ユーザーがどの結果をクリックしたか)や、精査されたQ&Aデータセットを用いて学習されます。関連するペアの類似度を高く、無関係なペアを低く評価するよう、厳密な最適化が行われています。

なぜこれが効果的なのか

類似検索が「形が似ていること」を追うのに対し、Two-Towerモデルは「この問いには、この答えが相応しい」という人間のような判断基準を直接学習します。その結果、言葉の表面をなぞるのではなく、文脈を汲み取った高度な検索が実現するのです。

まとめ

RAGの検索精度に関する悩みは、単なるパラメータの調整不足ではなく、類似検索という手法そのものの限界に起因しているケースが少なくありません。

Vertex AI Embedding APIのような学習済みモデルを賢く取り入れることで、検索システムの質を一段上のレベルへと引き上げることが可能です。

とある2日間に起こったトラブル

本記事は架空の出来事を基にしたフィクションです。実在の企業・団体・個人とは一切関係ありません。

事件から一年がたった。
ここに、あの2日間に起こった出来事の一部をまとめる。

前置き

当社は自社サービスを運営している。
サービスで使用するソフトウェアシステムの開発・保守を協力会社に依頼していた。
デプロイメントはCI/CDパイプラインを通じて自動化されており、本番環境へのリリースは常にビルド済みのコンテナイメージを使用する運用となっていた。

時系列

12月11日

[当社・協力会社] ミーティングを行い、翌日のリリースについて確認を行った。

リリース準備は万端だ

12月12日

13:19 [当社→協力会社] リリース作業が終わってもよい時間になっても協力会社から連絡がないため、リリース作業の進捗を確認する
13:23 [協力会社→当社] リリース内容を問い合わせ

すでに事前の合意内容と齟齬が生じていた。昨日のミーティングは何だったのか?

13:43 [当社→協力会社] リリース内容を回答する
13:47 [協力会社] リリースを開始する
14:00 [協力会社→当社] リリースについて問い合わせる

「いまさら?」という質問で驚く。事前にリリース準備をしていれば不要な問い合わせだった。

〜ちょっとごたつくが省略〜

15:32 [協力会社→当社] リリース完了を報告
15:33 [当社→協力会社] 不具合時の前バージョンへの切り戻しの準備を依頼する

今までの経験から、リリースがうまくいかない可能性は十分にある。対策に余念がない。

15:35 [当社] 不具合を確認

ある意味で予想通りである

15:38 [当社→協力会社] 前バージョンへの切り戻しの開始を依頼する

ここから始まる

15:43 [協力会社→当社] 前バージョンへの切り戻しを承諾する
15:51 [協力会社→当社] エラーの原因が判明したと報告し、本番環境の動作確認を依頼する

前バージョンへの切り戻しを依頼しているにもかかわらず、なぜエラー調査や動作確認の話になるのか理解できなかった。

15:53 [当社→協力会社] 本番環境のバージョンを確認する
15:54 [協力会社→当社] 新バージョンであることを報告

前バージョンへの切り戻しが実施されていなかったことが判明!
前バージョンへの切り戻しの代わりに、本番環境でエラー原因の調査を行っていた!!

15:55 [当社→協力会社] 再び前バージョンへの切り戻しを依頼する
15:58 [協力会社→当社] 前バージョンへの切り戻しを承諾

この判断の遅れによるタイムロスは非常に大きかった。

〜このあと大変なことになるが公開できない〜

12月13日

9:59 [協力会社] リリース作業を開始する
10:12 [協力会社→当社] リリース完了を報告
11:37 [当社] 検証作業を完了する

これまでの経緯もあり、慎重に検証を行った。
これでリリースの失敗はあり得ない。誰もがそう思った。

14:55 [当社] 問題を発見する。社内で緊急に対応を協議する

午前中に検証を行い、正常に動作することを確認していた機能が動作しない。
数時間前に動作していた機能が、なぜ突然動かなくなるのか。
慎重に検証を行っていたため、協力会社の作業手順そのものに問題があるとは考えていなかった。

〜このあと大変なことになるが公開できない〜

協力会社の報告書

本番リリースの検証完了後、本番環境にSSH接続してDockerコンテナ内のソースコードを直接編集し、不要と判断した関数を削除したところ、当該関数が使用中であったことが判明しました。

最後に

現在では開発・運用はすべて社内で行っており、
当時からすると信じられないほど安定したリリースができるようになっています。