-
-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Voter update #5908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Voter update #5908
Changes from 1 commit
20cead6
a4c7d6e
5d0e6b2
31f6e3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why changing the namespace to use If the application grows, it could have many things in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree partially with @ogizanagi. Why not using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As the best practice is to not clutter the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with @xabbuh. There is no perfect answer, but I do hate telling people to create one directory, then another directory inside that directory before doing something. |
||
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We must check if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need to have that check, fortunately :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, actually @wouterj already proved me wrong. :) |
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to check if |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should avoid using "we" here |
||
/** @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!'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The service should still be private. |
||
} | ||
} | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. boolean |
||
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 <declaring-the-voter-as-a-service>`. | ||
|
||
: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`` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extra backquote before |
||
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)`` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need a label for this headline. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks :) - there is a headline now, but it's further up on the page |
||
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"> | ||
|
||
<services> | ||
<service id="security.access.post_voter" | ||
<service id="app.post_voter" | ||
class="AppBundle\Security\Authorization\Voter\PostVoter" | ||
public="false" | ||
> | ||
|
@@ -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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. - $container->register('app.post_voter', 'AppBundle\Security\Authorization\Voter\PostVoter')
+ $container->register('app.post_voter', 'AppBundle\Security\PostVoter') if you decide to keep it in the |
||
->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('<h1>'.$post->getName().'</h1>'); | ||
} | ||
} | ||
|
||
.. 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 <how-to-use-the-voter-in-a-controller>`, | ||
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) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
version number is missing here