LoginSignup
11
10

More than 5 years have passed since last update.

[Symfony2] コントローラーでのエンティティのパーミッションチェックもアノテーションにしてしまえの巻

Last updated at Posted at 2014-12-01

どうもSecurityVoterの話です。
コントローラーみたいな分かりきったロジックを書くのが好きでないので前から好んで使ってるのですが、あまり使われていない(私の周りだけかも)様子なので布教がてら書いてみます。

SecurityVoterとは

あるデータに対して、属性の権限があるかどうかを定義します。
定義を追加することで、パーミッションのチェック機能を拡張できます。

ACLはリッチすぎますわー的な場合はマッチするかと思います。
そうでない場合でもあると便利ですよ。

さっそく作ってみる

ブログチュートリアルでよくある投稿とその著者があるパターン

エンティティ

use Symfony\Component\Security\Core\User\UserInterface;

class Post
{
    /**
     * @User
     */
    private $author;

    /**
     * @return User
     */
    public function getAuthor()
    {
        return $this->author;
    }
}

class User implements UserInterface
{
}

パーミッションのチェックを書きます。

PostSecurityVoter
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

class PostSecurityVoter implements VoterInterface
{
    // サポートする属性
    public function supportsAttribute($attribute)
    {
        return in_array($attribute, ['READ', 'UPDATE'], true);
    }

    // サポートするクラス
    public function supportsClass($class)
    {
        return 'User' === $class;
    }

    // 権限チェック
    public function vote(TokenInterface $token, $object, array $attributes)
    {
        if (!$this->supportsClass(get_class($object)) {
            return VoterInterface::ACCESS_ABSTAIN;
        }

        $user = $token->getUser();

        $result = VoterInterface::ACCESS_ABSTAIN;
        foreach ($attributes as $attribute) {
            if (!$this->supportsAttribute($attribute)) {
                continue;
            }

            $result = VoterInterface::ACCESS_DENIED;
            if ('READ' === $attribute) {
                // 誰でも観れる
                return VoterInterface::ACCESS_GRANTED;
            }

            if ('UPDATE' === $attribute && $user === $object->getAuthor()) {
                // 作者だけ編集できる
                return VoterInterface::ACCESS_GRANTED;
            }
        }

        return $result;
    }
}

注意点はVoterInterface::supportsAttribute()VoterInterface::supportsClass()いい感じに外部からコールされ、フィルタされそうな雰囲気を醸しつつもそうではない点 です。
こうなっている意図は知りませんが VoterInterface::vote() の中で自らサポートする属性/クラスのチェックをする必要があります。

symfony 2.6以降はAbstractVoterが登場したので、自らチェックする必要は無くなりました。
http://symfony.com/blog/new-in-symfony-2-6-simpler-security-voters

ちょっと長いですが実装内容は簡単で、属性に対するパーミッションのチェック結果(DENIED/GRANTED)を返すだけです。
関係ない場合はABSTAINを返し、他のSecurityVoterに委ねます。

補足:クラスや属性を指定する必要は特になく、例えばIPによるアクセス制限(公式ドキュメント)等も実装できます。

使ってみる

SecurityVoterはそのままでは何の役にも立たないので、サービス定義しておく必要が有ります

acme_demo.security_voter.post:
  class: PostSeurityVoter
  public: false
  tags:
    - { name: security.voter }

さっそくコントローラーの変貌を見てみましょう...

PostController_ビフォー

use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\View;

class PostController
{
    /**
     * @Route("/post/{id}/edit")
     * @ParamConverter("post")
     * @View()
     */
    public fucntion editAction(Post $post)
    {
        if ($post->getAuthor() === $this->getUser()) {
            throw $this->createNotFoundException();
        }

        return $post;
    }
}
PostController_アフター

use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\View;

class PostController
{
    /**
     * @Route("/post/{id}/edit")
     * @ParamConverter("post")
     * @Security("is_granted('UPDATE', post)")
     * @View()
     */
    public fucntion editAction(Post $post)
    {
        return $post;
    }
}

割と元からスッキリしてましたが、更にスッキリしました!

SecurityVoterによるパーミッションチェックは、SecurityContext等から行えるので、コントローラー以外にもtwigテンプレート内でも権限チェックできるようになるおまけつきです。
編集/削除 ボタンの表示とか何気に便利です。

11
10
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
11
10