7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SymfonyのVoterを実装して気づいた、ControllerとUseCaseの守備範囲

7
Last updated at Posted at 2025-12-30

遅くなりすみません🙇‍♂️
Symfony Advent Calendar 2025の24日目の記事です!

目次

概要

Symfonyでは#[IsGranted]を使うことで、Controllerで簡単に権限チェックを行うことができます。
私自身もこれまでは、既存のVoterを「使う側」として利用してきました。

今回はある機能実装をきっかけにVoterを作成する機会があり、その過程で

  • Voterが守ってくれる範囲
  • UseCase側で考えるべき安全性

について、改めて考えるきっかけになりました。
この記事ではそのときに感じたことを整理してみたいと思います。

まずは簡単にVoterについてと作成方法をまとめます。
すでにご存知の方は「Voterは入口しか守らない」 まで飛ばしていただいても大丈夫です。

Voterとは?

SymfonyのVoterは、「ある操作をそのユーザーが実行してよいか」を判定するための仕組みです。
ControllerやテンプレートなどからisGranted()を呼ぶと対応するVoterが実行され、許可(true)か拒否(false)が返されます。

Voterは、

  • 誰が
  • どのリソースに対して
  • どの操作をしてよいか

を判断してくれます。

今回はControllerのattributeを例にします。

#[IsGranted('EDIT', subject: 'resource')]

Voterの作成方法

#[IsGranted('EDIT', subject: 'resource')]と記述するとSymfonyは、

  • EDITという attribute
  • resource(subject)

を元に該当するVoterを探して評価します。

VoterはSymfony\Component\Security\Core\Authorization\Voter\Voterを拡張して、主に2つのメソッドを実装します。

  • supports():このVoterが対象とするattributesubjectかどうかを判断する

  • voteOnAttribute():実際に許可するかどうかの判定する

Controller側(呼び出し側)

更新用のControllerで、対象リソースを受け取りアノテーションでEDITを要求します。

use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

class Controller extends AbstractController
{
    #[IsGranted('EDIT', subject: 'resource', statusCode: 403)]
    #[Route(path: '/update/{id}', methods: ['PUT'])]
    public function update(Resource $resource): Response
    {
        // ここに来た時点で「EDIT」が可能と判定されている
        // メソッド内の更新処理はUseCaseに持たせる事とする

        return new Response(status: Response::HTTP_NO_CONTENT);
    }
}

更新処理に入る前にEDITの判定が走り、許可されない場合は403が返されます。

Voter側

VoterではResourceを対象に、「ログインユーザーがそのリソースを編集できるか」を判定します。

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class ResourceVoter extends Voter
{
    public const EDIT = 'EDIT';

    protected function supports(mixed $attribute, mixed $subject): bool
    {
        return $attribute === self::EDIT && $subject instanceof Resource;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();
        if (!$user instanceof User) {
            return false;
        }

        /** @var Resource $resource */
        $resource = $subject;

        // 例:所有者のみ編集可
        return $resource->getUser() === $user;
    }
}

上記では、以下の処理をしています。

  • supports()で対象を絞る
    • EDIT以外は処理しない
    • Resource以外は処理しない
  • voteOnAttribute()でユーザーとリソースから判定する
  • ログインユーザーが取得できない場合は拒否する
  • 所有者かどうかを判定する

これによってControllerは#[IsGranted('EDIT', subject: 'resource')]と書くだけで更新処理の前に権限の許可が走るようになります。

Voterは入口しか守らない

ここまでで、「更新は EDIT を通過した人だけが実行できる」という Controller(入口)での安全性 は実現できました。

ただ、今回の実装ではResourceの状態(status)を持っている仕様でした。

そのため、ControllerでEDITを通過してもUseCaseが常に安全とは限らないのでは? という疑問が生まれました。

EDITは通っているけど、業務的には更新してはいけない例

例えば、下記のようなUseCaseを考えます。

class UpdateResourceUseCase
{
    // ControllerでEDITは通っている前提
    public function handle(Resource $resource): void
    {
        $resource->updateSomething();

        $this->repository->save($resource);
    }
}

このUseCaseはシンプルですが、「Resourcestatusがどの状態か」を確認せずに更新しています。

もし仕様として、「statusfalseの場合は更新不可」というルールがあったとしても、Voterはすでに通過しているのでそのまま更新処理が実行されてしまいます。

なぜこの問題が起きるのか

ここで意識しておく必要があるのは、「Voterがどこで使われ、どこまでを守る仕組みなのか」という点です。

Voterの役割を整理すると、下記のようになります。

  • Voterが評価されるのは、Controllerを通過するタイミング
  • 入口での権限の確認を担当する
  • 実際の業務処理やデータ更新の中身には関与しない

つまりVoterは、「このリクエストを通してよいか」を判断するための仕組みであり、

  • この処理が業務的に正しいか
  • どこから呼ばれても安全か

までは保証しません。

VoterとUseCaseの役割

今回の実装を通して、改めて下記のように整理できました。
Voterは、

  • 誰がその操作をしてよいか
  • Controllerの入口を守る

UseCaseは、

  • 操作した結果が業務的に正しいか
  • 状態や不変条件を保証する

まとめ

今回はVoterを新しく実装したことをきっかけに、

  • Voterが守っている範囲
  • UseCase側で考えるべき安全性

について整理してみました。

VoterはControllerで権限の確認を行うための仕組みですが、それだけで業務処理の安全性が保証されるわけではなく、「誰が操作できるか」(Voter)と「操作した結果が正しいか」(UseCase)の役割分担を意識することで破綻しにくい設計になると感じました。

Voterを「使う側」から「実装する側」になったことで、設計について改めて考える良いきっかけになりました。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?