Contributed by
Roman Marintšenko
andRyan Weaver
in #11183.
Security Voters provide a mechanism to set up fine-grained restrictions in Symfony applications. The main advantage over ACLs is that they are an order of magnitude easier to set up, configure and use.
In previous Symfony versions, voters implemented the VoterInterface
interface, which has the following signature:
1 2 3 4 5 6 | interfaceVoterInterface{publicfunctionsupportsAttribute($attribute);publicfunctionsupportsClass($class);publicfunctionvote(TokenInterface$token,$object,array$attributes);} |
Implementing this interface is pretty easy, but the resulting code was usually a bit bloated, as demonstrated by the following 83 lines of code needed to define a simple voter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | // src/Acme/DemoBundle/Security/Authorization/Voter/PostVoter.phpnamespaceAcme\DemoBundle\Security\Authorization\Voter;useSymfony\Component\Security\Core\Authorization\Voter\VoterInterface;useSymfony\Component\Security\Core\Authentication\Token\TokenInterface;useSymfony\Component\Security\Core\User\UserInterface;classPostVoterimplementsVoterInterface{constVIEW='view';constEDIT='edit';publicfunctionsupportsAttribute($attribute){returnin_array($attribute,array(self::VIEW,self::EDIT,));}publicfunctionsupportsClass($class){$supportedClass='Acme\DemoBundle\Entity\Post';return$supportedClass===$class||is_subclass_of($class,$supportedClass);}/** * @var \Acme\DemoBundle\Entity\Post $post */publicfunctionvote(TokenInterface$token,$post,array$attributes){// check if class of this object is supported by this voterif(!$this->supportsClass(get_class($post))){returnVoterInterface::ACCESS_ABSTAIN;}// check if the voter is used correct, only allow one attribute// this isn't a requirement, it's just one easy way for you to// design your voterif(1!==count($attributes)){thrownew\InvalidArgumentException('Only one attribute is allowed for VIEW or EDIT');}// set the attribute to check against$attribute=$attributes[0];// check if the given attribute is covered by this voterif(!$this->supportsAttribute($attribute)){returnVoterInterface::ACCESS_ABSTAIN;}// get current logged in user$user=$token->getUser();// make sure there is a user object (i.e. that the user is logged in)if(!$userinstanceofUserInterface){returnVoterInterface::ACCESS_DENIED;}switch($attribute){caseself::VIEW:// the data object could have for example a method isPrivate()// which checks the Boolean attribute $privateif(!$post->isPrivate()){returnVoterInterface::ACCESS_GRANTED;}break;caseself::EDIT:// we assume that our data object has a method getOwner() to// get the current owner user entity for this data objectif($user->getId()===$post->getOwner()->getId()){returnVoterInterface::ACCESS_GRANTED;}break;}returnVoterInterface::ACCESS_DENIED;}} |
As the result of the Symfony DX initiative, Symfony 2.6 will allow to define
much simpler security voters. To do so, use the new AbstractVoter
class
which implements VoterInterface
and defines the following methods:
1 2 3 4 5 6 7 8 9 | abstractclassAbstractVoterimplementsVoterInterface{publicfunctionsupportsAttribute($attribute);publicfunctionsupportsClass($class);publicfunctionvote(TokenInterface$token,$object,array$attributes);abstractprotectedfunctiongetSupportedClasses();abstractprotectedfunctiongetSupportedAttributes();abstractprotectedfunctionisGranted($attribute,$object,$user=null);} |
The three methods supportsAttribute()
, supportsClass()
and vote()
help
you reduce the boilerplate code of the voter and let you focus on the specific
business logic of your application. As a result, the same voter shown above now
takes only 41 lines of code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | // src/Acme/DemoBundle/Security/Authorization/Voter/PostVoter.phpnamespaceAcme\DemoBundle\Security\Authorization\Voter;useSymfony\Component\Security\Core\Authorization\Voter\AbstractVoter;useSymfony\Component\Security\Core\User\UserInterface;classPostVoterextendsAbstractVoter{constVIEW='view';constEDIT='edit';protectedfunctiongetSupportedAttributes(){returnarray(self::VIEW,self::EDIT);}protectedfunctiongetSupportedClasses(){returnarray('Acme\DemoBundle\Entity\Post');}protectedfunctionisGranted($attribute,$post,$user=null){// make sure there is a user object (i.e. that the user is logged in)if(!$userinstanceofUserInterface){returnfalse;}// custom business logic to decide if the given user can view// and/or edit the given postif($attribute==self::VIEW&&!$post->isPrivate()){returntrue;}if($attribute==self::EDIT&&$user->getId()===$post->getOwner()->getId()){returntrue;}returnfalse;}} |
Writing less code to get the same results as before boosts your productivity. That's our obsession since the introduction of the DX initiative and Symfony 2.6 will be the first version to embrace this new philosophy.