[Symfony] Security Voterを使って「リソースの所有者でないと編集不可」を実装してみよう
Symfony Advent Calendar 2020 の13日目の記事です!🎄🌙
昨日は @ippey_s さんの ローカル開発でSymfony CLIがすごい でした✨
ちなみに、僕はよく TwitterにもSymfonyネタを呟いている ので、よろしければぜひ フォローしてやってください🕊🤲
はじめに
Symfonyでコントローラのアクションにアクセス制限を設ける場合、よく使うのは @IsGranted アノテーションだと思います。
/**
* @Route("foo/{id}/edit", name="foo_edit", methods={"GET", "POST"})
* @IsGranted("ROLE_ALLOWED_TO_EDIT")
*/
public function edit(Request $request, Foo $foo)
{
// ...
}
では、例えば ユーザー自身が所有するリソースなら編集可能だけど、他人のリソースは編集不可 という要件を実装する場合はどうすればいいでしょうか?
/**
* @Route("foo/{id}/edit", name="foo_edit", methods={"GET", "POST"})
*/
public function edit(Request $request, Foo $foo)
{
if (!$foo->user !== $this->getUser()) {
throw new AccessDeniedHttpException();
}
// ...
}
こんな実装を想像した方もいるかもしれません。
別に間違った実装ではないのですが、実はこういうケースでは、Symfonyの Security Voter という機能を使うことでアクセス可否の判定をきれいにモジュール化できます👍
というわけでこのケースを例にSecurity Voterの具体的な使い方を解説してみたいと思います。
1. 実際に判定を行うVoterクラスを実装する
まず、実際にアクセス可否の判定を行うVoterクラスを実装します。
以下のように Symfony\Component\Security\Core\Authorization\Voter\Voter
クラスを継承して実装することで、フレームワークが自動でVoterと認識してくれるので、 services.yaml
に設定を追記したりする必要はありません👌
公式ドキュメントのサンプルは こちら
<?php
// src/Security/Voter/FooVoter.php
declare(strict_types=1);
namespace App\Security\Voter;
use App\Entity\Foo;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class FooVoter extends Voter
{
const EDIT = 'EDIT';
protected function supports($attribute, $subject)
{
// 定義済みの属性しか指定できないように
if (!in_array($attribute, [self::EDIT])) {
return false;
}
// この Voter は Foo インスタンスだけを対象とする
if (!$subject instanceof Foo) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
// ログイン済みユーザーであることを保証
if (!$user instanceof User) {
return false;
}
switch ($attribute) {
case self::EDIT:
// $subject の中身は Foo インスタンスであることが supports() メソッドによって保証されている
return $this->canEdit($subject, $user);
}
// この行まで到達することがあればcase文が漏れている
throw new \LogicException();
}
// EDIT 属性についての判定処理
private function canEdit(Foo $foo, User $user)
{
return $foo->user === $user;
}
}
この例では、
-
Foo
インスタンスのみを対象とするVoterを作り、 -
'EDIT'
という属性を定義し、 -
'EDIT'
属性についてのアクセス可否のロジック(インスタンスの所有者かどうか)を実装
しています。
@IsGranted
アノテーションや is_granted()
Twig関数を使って判定
2. Voterを適切に実装すれば、あとは何もしなくても @IsGranted
アノテーションや is_granted()
Twig関数で普通に判定ができます。
/**
* @Route("foo/{id}/edit", name="foo_edit", methods={"GET", "POST"})
* @IsGranted("EDIT", subject="foo", statusCode=403)
*/
public function edit(Request $request, Foo $foo)
{
// ...
}
{% if is_granted('EDIT', foo) %}
<a href="{{ path('foo_edit', {id: foo.id}) }}">編集</a>
{% endif %}
便利!😳
なお、 @IsGranted
の subject="foo"
の "foo"
は、アクションメソッドの引数の変数名 $foo
に一致させる必要があります。
また、 statusCode=403
は任意の引数です。明示的に指定しない場合のデフォルトの挙動では 302
でログイン画面にリダイレクトされます。
おわりに
Symfonyの便利な Security Voter の使い方について簡単に解説しました。お役に立てば幸いです!
Symfony Advent Calendar 2020、明日は @77web さんです!お楽しみに!
Discussion