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


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

## SecurityVoterとは

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

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

## さっそく作ってみる

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

```php:エンティティ

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

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

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

class User implements UserInterface
{
}
```

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


```php: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によるアクセス制限(公式ドキュメント)](http://symfony.com/doc/current/cookbook/security/voters.html)等も実装できます。

### 使ってみる

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

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

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

```php: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;
    }
}
```

```php: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テンプレート内でも権限チェックできる](http://symfony.com/doc/current/book/security.html#access-control-in-templates)ようになるおまけつきです。
編集/削除 ボタンの表示とか何気に便利です。