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:: 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