diff --git a/cookbook/security/voters.rst b/cookbook/security/voters.rst index 9b7be023bd8..95ba265375d 100644 --- a/cookbook/security/voters.rst +++ b/cookbook/security/voters.rst @@ -35,120 +35,179 @@ The Voter Interface A custom voter needs to implement :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface` -or extend :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter`, +or extend :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\Voter`, which makes creating a voter even easier. .. code-block:: php - abstract class AbstractVoter implements VoterInterface + abstract class Voter implements VoterInterface { - abstract protected function getSupportedClasses(); - abstract protected function getSupportedAttributes(); - abstract protected function isGranted($attribute, $object, $user = null); + abstract protected function supports($attribute, $subject); + abstract protected function voteOnAttribute($attribute, $subject, TokenInterface $token); } -In this example, the voter will check if the user has access to a specific -object according to your custom conditions (e.g. they must be the owner of -the object). If the condition fails, you'll return -``VoterInterface::ACCESS_DENIED``, otherwise you'll return -``VoterInterface::ACCESS_GRANTED``. In case the responsibility for this decision -does not belong to this voter, it will return ``VoterInterface::ACCESS_ABSTAIN``. +.. versionadded:: + The ``Voter`` helper class was added in Symfony 2.8. In early versions, an + ``AbstractVoter`` class with similar behavior was available. + +.. _how-to-use-the-voter-in-a-controller: + +Setup: Checking for Access in a Controller +------------------------------------------ + +Suppose you have a ``Post`` object and you need to decide whether or not the current +user can *edit* or *view* the object. In your controller, you'll check access with +code like this:: + + // src/AppBundle/Controller/PostController.php + // ... + + class PostController extends Controller + { + /** + * @Route("/posts/{id}", name="post_show") + */ + public function showAction($id) + { + // get a Post object - e.g. query for it + $post = ...; + + // check for "view" access: calls all voters + $this->denyAccessUnlessGranted('view', $post); + + // ... + } + + /** + * @Route("/posts/{id}/edit", name="post_edit") + */ + public function editAction($id) + { + // get a Post object - e.g. query for it + $post = ...; + + // check for "edit" access: calls all voters + $this->denyAccessUnlessGranted('edit', $post); + + // ... + } + } + +The ``denyAccessUnlessGranted()`` method (and also, the simpler ``isGranted()`` method) +calls out to the "voter" system. Right now, no voters will vote on whether or not +the user can "view" or "edit" a ``Post``. But you can create your *own* voter that +decides this using whatever logic you want. + +.. tip:: + + The ``denyAccessUnlessGranted()`` function and the ``isGranted()`` functions + are both just shortcuts to call ``isGranted()`` on the ``security.authorization_checker`` + service. Creating the custom Voter ------------------------- -The goal is to create a voter that checks if a user has access to view or -edit a particular object. Here's an example implementation: +Suppose the logic to decide if a user can "view" or "edit" a ``Post`` object is +pretty complex. For example, a ``User`` can always edit or view a ``Post`` they created. +And if a ``Post`` is marked as "public", anyone can view it. A voter for this situation +would look like this:: -.. code-block:: php - - // src/AppBundle/Security/Authorization/Voter/PostVoter.php - namespace AppBundle\Security\Authorization\Voter; + // src/AppBundle/Security/PostVoter.php + namespace AppBundle\Security; - use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter; + use AppBundle\Entity\Post; use AppBundle\Entity\User; - use Symfony\Component\Security\Core\User\UserInterface; + use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Authorization\Voter\Voter; - class PostVoter extends AbstractVoter + class PostVoter extends Voter { + // these strings are just invented: you can use anything const VIEW = 'view'; const EDIT = 'edit'; - protected function getSupportedAttributes() + protected function supports($attribute, $subject) { - return array(self::VIEW, self::EDIT); - } + // if the attribute isn't one we support, return false + if (!in_array($attribute, array(self::VIEW, self::EDIT))) { + return false; + } - protected function getSupportedClasses() - { - return array('AppBundle\Entity\Post'); + // only vote on Post objects inside this voter + if (!$subject instanceof Post) { + return false; + } + + return true; } - protected function isGranted($attribute, $post, $user = null) + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) { - // make sure there is a user object (i.e. that the user is logged in) - if (!$user instanceof UserInterface) { - return false; - } + $user = $token->getUser(); - // double-check that the User object is the expected entity (this - // only happens when you did not configure the security system properly) if (!$user instanceof User) { - throw new \LogicException('The user is somehow not our User class!'); + // the user must not be logged in, so we deny access + return false; } + // we know $subject is a Post object, thanks to supports + /** @var Post $post */ + $post = $subject; + switch($attribute) { case self::VIEW: - // the data object could have for example a method isPrivate() - // which checks the Boolean attribute $private - if (!$post->isPrivate()) { - return true; - } - - break; + return $this->canView($post, $user); case self::EDIT: - // this assumes that the data object has a getOwner() method - // to get the entity of the user who owns this data object - if ($user->getId() === $post->getOwner()->getId()) { - return true; - } - - break; + return $this->canEdit($post, $user); } - return false; + throw new \LogicException('This code should not be reached!'); } - } -That's it! The voter is done. The next step is to inject the voter into -the security layer. + private function canView(Post $post, User $user) + { + // if they can edit, they can view + if ($this->canEdit($post, $user)) { + return true; + } + + // the Post object could have, for example, a method isPrivate() + // that checks a Boolean $private property + return !$post->isPrivate(); + } -To recap, here's what's expected from the three abstract methods: + private function canEdit(Post $post, User $user) + { + // this assumes that the data object has a getOwner() method + // to get the entity of the user who owns this data object + return $user === $post->getOwner(); + } + } -:method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter::getSupportedClasses` - It tells Symfony that your voter should be called whenever an object of one - of the given classes is passed to ``isGranted()``. For example, if you return - ``array('AppBundle\Model\Product')``, Symfony will call your voter when a - ``Product`` object is passed to ``isGranted()``. +That's it! The voter is done! Next, :ref:`configure it `. -:method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter::getSupportedAttributes` - It tells Symfony that your voter should be called whenever one of these - strings is passed as the first argument to ``isGranted()``. For example, if - you return ``array('CREATE', 'READ')``, then Symfony will call your voter - when one of these is passed to ``isGranted()``. +To recap, here's what's expected from the two abstract methods: -:method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter::isGranted` - It implements the business logic that verifies whether or not a given user is - allowed access to a given attribute (e.g. ``CREATE`` or ``READ``) on a given - object. This method must return a boolean. +``Voter::supports($attribute, $subject)`` + When ``isGranted()`` (or ``denyAccessUnlessGranted()``) is called, the first + argument is passed here as ``$attribute`` (e.g. ``ROLE_USER``, ``edit``) and + the second argument (if any) is passed as ```$subject`` (e.g. ``null``, a ``Post`` + object). Your job is to determine if your voter should vote on the attribute/subject + combination. If you return true, ``voteOnAttribute()`` will be called. Otherwise, + your voter is done: some other voter should process this. In this example, you + return ``true`` if the attribue is ``view`` or ``edit`` and if the object is + a ``Post`` instance. -.. note:: +``voteOnAttribute($attribute, $subject, TokenInterface $token)`` + If you return ``true`` from ``supports()``, then this method is called. Your + job is simple: return ``true`` to allow access and ``false`` to deny access. + The ``$token`` can be used to find the current user object (if any). In this + example, all of the complex business logic is included to determine access. - Currently, to use the ``AbstractVoter`` base class, you must be creating a - voter where an object is always passed to ``isGranted()``. +.. _declaring-the-voter-as-a-service: -Declaring the Voter as a Service --------------------------------- +Configuring the Voter +--------------------- To inject the voter into the security layer, you must declare it as a service and tag it with ``security.voter``: @@ -159,9 +218,8 @@ and tag it with ``security.voter``: # app/config/services.yml services: - security.access.post_voter: - class: AppBundle\Security\Authorization\Voter\PostVoter - public: false + app.post_voter: + class: AppBundle\Security\PostVoter tags: - { name: security.voter } @@ -175,7 +233,7 @@ and tag it with ``security.voter``: http://symfony.com/schema/dic/services/services-1.0.xsd"> - @@ -190,61 +248,27 @@ and tag it with ``security.voter``: // app/config/services.php use Symfony\Component\DependencyInjection\Definition; - $definition = new Definition('AppBundle\Security\Authorization\Voter\PostVoter'); - $definition + $container->register('app.post_voter', 'AppBundle\Security\Authorization\Voter\PostVoter') ->setPublic(false) ->addTag('security.voter') ; - $container->setDefinition('security.access.post_voter', $definition); - -How to Use the Voter in a Controller ------------------------------------- - -The registered voter will then always be asked as soon as the method ``isGranted()`` -from the authorization checker is called. When extending the base ``Controller`` -class, you can simply call the -:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::denyAccessUnlessGranted()` -method:: - - // src/AppBundle/Controller/PostController.php - namespace AppBundle\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\HttpFoundation\Response; - - class PostController extends Controller - { - public function showAction($id) - { - // get a Post instance - $post = ...; - - // keep in mind that this will call all registered security voters - $this->denyAccessUnlessGranted('view', $post, 'Unauthorized access!'); - - return new Response('

'.$post->getName().'

'); - } - } - -.. versionadded:: 2.6 - The ``denyAccessUnlessGranted()`` method was introduced in Symfony 2.6. - Prior to Symfony 2.6, you had to call the ``isGranted()`` method of the - ``security.context`` service and throw the exception yourself. - -It's that easy! +You're done! Now, when you :ref:`call isGranted() with view/edit and a Post object `, +your voter will be executed and you can control access. .. _security-voters-change-strategy: Changing the Access Decision Strategy ------------------------------------- -Imagine you have multiple voters for one action for an object. For instance, -you have one voter that checks if the user is a member of the site and a second -one checking if the user is older than 18. +Normally, only one voter will vote at any given time (the rest will "abstain", which +means they return ``false`` from ``supports()``). But in theory, you could make multiple +voters vote for one action and object. For instance, suppose you have one voter that +checks if the user is a member of the site and a second one that checks if the user +is older than 18. To handle these cases, the access decision manager uses an access decision -strategy. You can configure this to suite your needs. There are three +strategy. You can configure this to suit your needs. There are three strategies available: ``affirmative`` (default)