diff --git a/cookbook/security/voters.rst b/cookbook/security/voters.rst index 9b7be023bd8..1750f2e8025 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:: 2.8 + The ``Voter`` helper class was added in Symfony 2.8. In earlier 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 be logged in; if not, deny access + return false; } + // you 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,11 +218,12 @@ 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 } + # small performance boost + public: false .. code-block:: xml @@ -175,8 +235,8 @@ and tag it with ``security.voter``: http://symfony.com/schema/dic/services/services-1.0.xsd"> - @@ -190,61 +250,130 @@ 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\PostVoter') ->setPublic(false) ->addTag('security.voter') ; - $container->setDefinition('security.access.post_voter', $definition); +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. -How to Use the Voter in a Controller ------------------------------------- +Checking for Roles inside a Voter +--------------------------------- -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:: +.. versionadded:: 2.8 + The ability to inject the ``AccessDecisionManager`` is new in 2.8: it caused + a CircularReferenceException before. In earlier versions, you must inject the + ``service_container`` itself and fetch out the ``security.authorization_checker`` + to use ``isGranted()``. - // src/AppBundle/Controller/PostController.php - namespace AppBundle\Controller; +What if you want to call ``isGranted()`` from *inside* your voter - e.g. you want +to see if the current user has ``ROLE_SUPER_ADMIN``. That's possible by injecting +the :class:`Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager` +into your voter. You can use this to, for example, *always* allow access to a user +with ``ROLE_SUPER_ADMIN``:: - use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use Symfony\Component\HttpFoundation\Response; + // src/AppBundle/Security/PostVoter.php - class PostController extends Controller + // ... + use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; + + class PostVoter extends Voter { - public function showAction($id) + // ... + + private $decisionManager; + + public function __construct(AccessDecisionManagerInterface $decisionManager) { - // get a Post instance - $post = ...; + $this->decisionManager = $decisionManager; + } - // keep in mind that this will call all registered security voters - $this->denyAccessUnlessGranted('view', $post, 'Unauthorized access!'); + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + // ... + + // ROLE_SUPER_ADMIN can do anything! The power! + if ($this->decisionManager->decide($token, array('ROLE_SUPER_ADMIN'))) { + return true; + } - return new Response('

'.$post->getName().'

'); + // ... all the normal voter logic } } -.. 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. +Next, update ``services.yml`` to inject the ``security.access.decision_manager`` +service: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + app.post_voter: + class: AppBundle\Security\PostVoter + arguments: ['@security.access.decision_manager'] + public: false + tags: + - { name: security.voter } + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/services.php + use Symfony\Component\DependencyInjection\Definition; + use Symfony\Component\DependencyInjection\Reference; + + $container->register('app.post_voter', 'AppBundle\Security\PostVoter') + ->addArgument(new Reference('security.access.decision_manager')) + ->setPublic(false) + ->addTag('security.voter') + ; + +That's it! Calling ``decide()`` on the ``AccessDecisionManager`` is essentially +the same as calling ``isGranted()`` from a controller or other places +(it's just a little lower-level, which is necessary for a voter). + +.. note:: -It's that easy! + The ``security.access.decision_manager`` is private. This means you can't access + it directly from a controller: you can only inject it into other services. That's + ok: use ``security.authorization_checker`` instead in all cases except for voters. .. _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) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy