From 20cead68da3dff710815da31a5301130c5bba28f Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Fri, 27 Nov 2015 16:01:53 -0500 Subject: [PATCH 1/4] Reworking the voter article for the new Voter class --- cookbook/security/voters.rst | 262 +++++++++++++++++++---------------- 1 file changed, 143 insertions(+), 119 deletions(-) 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) From a4c7d6e4ca12b96aa05d309d50929c48fd7b4b1b Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Fri, 27 Nov 2015 17:48:26 -0500 Subject: [PATCH 2/4] adding a section about calling isGranted() from within a voter --- cookbook/security/voters.rst | 101 +++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/cookbook/security/voters.rst b/cookbook/security/voters.rst index 95ba265375d..59ba7d5bd04 100644 --- a/cookbook/security/voters.rst +++ b/cookbook/security/voters.rst @@ -256,6 +256,107 @@ and tag it with ``security.voter``: 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. +Checking for Roles inside a Voter +--------------------------------- + +.. 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()``. + +What if you want to call ``isGranted()`` fomr *inside* your voter - e.g. you want +to see if the current user has ``ROLE_SUPER_ADMIN``. That's possible by injecting +the ``AccessDecisionManager`` into your voter. You can use this to, for example, +*always* allow access to a user with ``ROLE_SUPER_ADMIN``:: + + // src/AppBundle/Security/PostVoter.php + // ... + + use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; + + class PostVoter extends Voter + { + // ... + + private $decisionManager; + + public function __construct(AccessDecisionManagerInterface $decisionManager) + { + $this->decisionManager = $decisionManager; + } + + 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; + } + + // ... all the normal voter logic + } + } + +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'] + 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\Authorization\Voter\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()`` on the normal ``security.authorization_checker`` +service (it's just a little lower-level, which is necessary for a voter). + +.. note:: + + 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 From 5d0e6b279b837881b12a5e138b66d170db6d9d43 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Fri, 27 Nov 2015 17:51:07 -0500 Subject: [PATCH 3/4] tweaks thanks to Javier --- cookbook/security/voters.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cookbook/security/voters.rst b/cookbook/security/voters.rst index 59ba7d5bd04..1d43feddf7a 100644 --- a/cookbook/security/voters.rst +++ b/cookbook/security/voters.rst @@ -47,7 +47,7 @@ which makes creating a voter even easier. } .. versionadded:: - The ``Voter`` helper class was added in Symfony 2.8. In early versions, an + 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: @@ -146,7 +146,7 @@ would look like this:: $user = $token->getUser(); if (!$user instanceof User) { - // the user must not be logged in, so we deny access + // the user must be logged in; if not, deny access return false; } From 31f6e3dced4d802a20981589d6cc2971e8e38a22 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Sun, 29 Nov 2015 23:08:17 -0500 Subject: [PATCH 4/4] Many tweaks thanks to a great review --- cookbook/security/voters.rst | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/cookbook/security/voters.rst b/cookbook/security/voters.rst index 1d43feddf7a..1750f2e8025 100644 --- a/cookbook/security/voters.rst +++ b/cookbook/security/voters.rst @@ -46,7 +46,7 @@ which makes creating a voter even easier. abstract protected function voteOnAttribute($attribute, $subject, TokenInterface $token); } -.. versionadded:: +.. versionadded:: 2.8 The ``Voter`` helper class was added in Symfony 2.8. In earlier versions, an ``AbstractVoter`` class with similar behavior was available. @@ -150,7 +150,7 @@ would look like this:: return false; } - // we know $subject is a Post object, thanks to supports + // you know $subject is a Post object, thanks to supports /** @var Post $post */ $post = $subject; @@ -172,7 +172,7 @@ would look like this:: } // the Post object could have, for example, a method isPrivate() - // that checks a Boolean $private property + // that checks a boolean $private property return !$post->isPrivate(); } @@ -191,7 +191,7 @@ To recap, here's what's expected from the two abstract methods: ``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`` + 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 @@ -222,6 +222,8 @@ and tag it with ``security.voter``: class: AppBundle\Security\PostVoter tags: - { name: security.voter } + # small performance boost + public: false .. code-block:: xml @@ -234,7 +236,7 @@ and tag it with ``security.voter``: @@ -248,7 +250,7 @@ and tag it with ``security.voter``: // app/config/services.php use Symfony\Component\DependencyInjection\Definition; - $container->register('app.post_voter', 'AppBundle\Security\Authorization\Voter\PostVoter') + $container->register('app.post_voter', 'AppBundle\Security\PostVoter') ->setPublic(false) ->addTag('security.voter') ; @@ -265,14 +267,15 @@ Checking for Roles inside a Voter ``service_container`` itself and fetch out the ``security.authorization_checker`` to use ``isGranted()``. -What if you want to call ``isGranted()`` fomr *inside* your voter - e.g. you want +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 ``AccessDecisionManager`` into your voter. You can use this to, for example, -*always* allow access to a user with ``ROLE_SUPER_ADMIN``:: +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``:: // src/AppBundle/Security/PostVoter.php - // ... + // ... use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; class PostVoter extends Voter @@ -311,6 +314,7 @@ service: app.post_voter: class: AppBundle\Security\PostVoter arguments: ['@security.access.decision_manager'] + public: false tags: - { name: security.voter } @@ -325,7 +329,7 @@ service: @@ -341,15 +345,15 @@ service: use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; - $container->register('app.post_voter', 'AppBundle\Security\Authorization\Voter\PostVoter') + $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()`` on the normal ``security.authorization_checker`` -service (it's just a little lower-level, which is necessary for a voter). +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::