どうも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
{
}
パーミッションのチェックを書きます。
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 }
さっそくコントローラーの変貌を見てみましょう...
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;
}
}
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テンプレート内でも権限チェックできるようになるおまけつきです。
編集/削除 ボタンの表示とか何気に便利です。