Skip to content

Commit 20cead6

Browse files
committed
Reworking the voter article for the new Voter class
1 parent 3ebf2d0 commit 20cead6

File tree

1 file changed

+143
-119
lines changed

1 file changed

+143
-119
lines changed

cookbook/security/voters.rst

Lines changed: 143 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -35,120 +35,179 @@ The Voter Interface
3535

3636
A custom voter needs to implement
3737
:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`
38-
or extend :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter`,
38+
or extend :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\Voter`,
3939
which makes creating a voter even easier.
4040

4141
.. code-block:: php
4242
43-
abstract class AbstractVoter implements VoterInterface
43+
abstract class Voter implements VoterInterface
4444
{
45-
abstract protected function getSupportedClasses();
46-
abstract protected function getSupportedAttributes();
47-
abstract protected function isGranted($attribute, $object, $user = null);
45+
abstract protected function supports($attribute, $subject);
46+
abstract protected function voteOnAttribute($attribute, $subject, TokenInterface $token);
4847
}
4948
50-
In this example, the voter will check if the user has access to a specific
51-
object according to your custom conditions (e.g. they must be the owner of
52-
the object). If the condition fails, you'll return
53-
``VoterInterface::ACCESS_DENIED``, otherwise you'll return
54-
``VoterInterface::ACCESS_GRANTED``. In case the responsibility for this decision
55-
does not belong to this voter, it will return ``VoterInterface::ACCESS_ABSTAIN``.
49+
.. versionadded::
50+
The ``Voter`` helper class was added in Symfony 2.8. In early versions, an
51+
``AbstractVoter`` class with similar behavior was available.
52+
53+
.. _how-to-use-the-voter-in-a-controller:
54+
55+
Setup: Checking for Access in a Controller
56+
------------------------------------------
57+
58+
Suppose you have a ``Post`` object and you need to decide whether or not the current
59+
user can *edit* or *view* the object. In your controller, you'll check access with
60+
code like this::
61+
62+
// src/AppBundle/Controller/PostController.php
63+
// ...
64+
65+
class PostController extends Controller
66+
{
67+
/**
68+
* @Route("/posts/{id}", name="post_show")
69+
*/
70+
public function showAction($id)
71+
{
72+
// get a Post object - e.g. query for it
73+
$post = ...;
74+
75+
// check for "view" access: calls all voters
76+
$this->denyAccessUnlessGranted('view', $post);
77+
78+
// ...
79+
}
80+
81+
/**
82+
* @Route("/posts/{id}/edit", name="post_edit")
83+
*/
84+
public function editAction($id)
85+
{
86+
// get a Post object - e.g. query for it
87+
$post = ...;
88+
89+
// check for "edit" access: calls all voters
90+
$this->denyAccessUnlessGranted('edit', $post);
91+
92+
// ...
93+
}
94+
}
95+
96+
The ``denyAccessUnlessGranted()`` method (and also, the simpler ``isGranted()`` method)
97+
calls out to the "voter" system. Right now, no voters will vote on whether or not
98+
the user can "view" or "edit" a ``Post``. But you can create your *own* voter that
99+
decides this using whatever logic you want.
100+
101+
.. tip::
102+
103+
The ``denyAccessUnlessGranted()`` function and the ``isGranted()`` functions
104+
are both just shortcuts to call ``isGranted()`` on the ``security.authorization_checker``
105+
service.
56106

57107
Creating the custom Voter
58108
-------------------------
59109

60-
The goal is to create a voter that checks if a user has access to view or
61-
edit a particular object. Here's an example implementation:
110+
Suppose the logic to decide if a user can "view" or "edit" a ``Post`` object is
111+
pretty complex. For example, a ``User`` can always edit or view a ``Post`` they created.
112+
And if a ``Post`` is marked as "public", anyone can view it. A voter for this situation
113+
would look like this::
62114

63-
.. code-block:: php
64-
65-
// src/AppBundle/Security/Authorization/Voter/PostVoter.php
66-
namespace AppBundle\Security\Authorization\Voter;
115+
// src/AppBundle/Security/PostVoter.php
116+
namespace AppBundle\Security;
67117

68-
use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter;
118+
use AppBundle\Entity\Post;
69119
use AppBundle\Entity\User;
70-
use Symfony\Component\Security\Core\User\UserInterface;
120+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
121+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
71122

72-
class PostVoter extends AbstractVoter
123+
class PostVoter extends Voter
73124
{
125+
// these strings are just invented: you can use anything
74126
const VIEW = 'view';
75127
const EDIT = 'edit';
76128

77-
protected function getSupportedAttributes()
129+
protected function supports($attribute, $subject)
78130
{
79-
return array(self::VIEW, self::EDIT);
80-
}
131+
// if the attribute isn't one we support, return false
132+
if (!in_array($attribute, array(self::VIEW, self::EDIT))) {
133+
return false;
134+
}
81135

82-
protected function getSupportedClasses()
83-
{
84-
return array('AppBundle\Entity\Post');
136+
// only vote on Post objects inside this voter
137+
if (!$subject instanceof Post) {
138+
return false;
139+
}
140+
141+
return true;
85142
}
86143

87-
protected function isGranted($attribute, $post, $user = null)
144+
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
88145
{
89-
// make sure there is a user object (i.e. that the user is logged in)
90-
if (!$user instanceof UserInterface) {
91-
return false;
92-
}
146+
$user = $token->getUser();
93147

94-
// double-check that the User object is the expected entity (this
95-
// only happens when you did not configure the security system properly)
96148
if (!$user instanceof User) {
97-
throw new \LogicException('The user is somehow not our User class!');
149+
// the user must not be logged in, so we deny access
150+
return false;
98151
}
99152

153+
// we know $subject is a Post object, thanks to supports
154+
/** @var Post $post */
155+
$post = $subject;
156+
100157
switch($attribute) {
101158
case self::VIEW:
102-
// the data object could have for example a method isPrivate()
103-
// which checks the Boolean attribute $private
104-
if (!$post->isPrivate()) {
105-
return true;
106-
}
107-
108-
break;
159+
return $this->canView($post, $user);
109160
case self::EDIT:
110-
// this assumes that the data object has a getOwner() method
111-
// to get the entity of the user who owns this data object
112-
if ($user->getId() === $post->getOwner()->getId()) {
113-
return true;
114-
}
115-
116-
break;
161+
return $this->canEdit($post, $user);
117162
}
118163

119-
return false;
164+
throw new \LogicException('This code should not be reached!');
120165
}
121-
}
122166

123-
That's it! The voter is done. The next step is to inject the voter into
124-
the security layer.
167+
private function canView(Post $post, User $user)
168+
{
169+
// if they can edit, they can view
170+
if ($this->canEdit($post, $user)) {
171+
return true;
172+
}
173+
174+
// the Post object could have, for example, a method isPrivate()
175+
// that checks a Boolean $private property
176+
return !$post->isPrivate();
177+
}
125178

126-
To recap, here's what's expected from the three abstract methods:
179+
private function canEdit(Post $post, User $user)
180+
{
181+
// this assumes that the data object has a getOwner() method
182+
// to get the entity of the user who owns this data object
183+
return $user === $post->getOwner();
184+
}
185+
}
127186

128-
:method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter::getSupportedClasses`
129-
It tells Symfony that your voter should be called whenever an object of one
130-
of the given classes is passed to ``isGranted()``. For example, if you return
131-
``array('AppBundle\Model\Product')``, Symfony will call your voter when a
132-
``Product`` object is passed to ``isGranted()``.
187+
That's it! The voter is done! Next, :ref:`configure it <declaring-the-voter-as-a-service>`.
133188

134-
:method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter::getSupportedAttributes`
135-
It tells Symfony that your voter should be called whenever one of these
136-
strings is passed as the first argument to ``isGranted()``. For example, if
137-
you return ``array('CREATE', 'READ')``, then Symfony will call your voter
138-
when one of these is passed to ``isGranted()``.
189+
To recap, here's what's expected from the two abstract methods:
139190

140-
:method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter::isGranted`
141-
It implements the business logic that verifies whether or not a given user is
142-
allowed access to a given attribute (e.g. ``CREATE`` or ``READ``) on a given
143-
object. This method must return a boolean.
191+
``Voter::supports($attribute, $subject)``
192+
When ``isGranted()`` (or ``denyAccessUnlessGranted()``) is called, the first
193+
argument is passed here as ``$attribute`` (e.g. ``ROLE_USER``, ``edit``) and
194+
the second argument (if any) is passed as ```$subject`` (e.g. ``null``, a ``Post``
195+
object). Your job is to determine if your voter should vote on the attribute/subject
196+
combination. If you return true, ``voteOnAttribute()`` will be called. Otherwise,
197+
your voter is done: some other voter should process this. In this example, you
198+
return ``true`` if the attribue is ``view`` or ``edit`` and if the object is
199+
a ``Post`` instance.
144200

145-
.. note::
201+
``voteOnAttribute($attribute, $subject, TokenInterface $token)``
202+
If you return ``true`` from ``supports()``, then this method is called. Your
203+
job is simple: return ``true`` to allow access and ``false`` to deny access.
204+
The ``$token`` can be used to find the current user object (if any). In this
205+
example, all of the complex business logic is included to determine access.
146206

147-
Currently, to use the ``AbstractVoter`` base class, you must be creating a
148-
voter where an object is always passed to ``isGranted()``.
207+
.. _declaring-the-voter-as-a-service:
149208

150-
Declaring the Voter as a Service
151-
--------------------------------
209+
Configuring the Voter
210+
---------------------
152211

153212
To inject the voter into the security layer, you must declare it as a service
154213
and tag it with ``security.voter``:
@@ -159,9 +218,8 @@ and tag it with ``security.voter``:
159218
160219
# app/config/services.yml
161220
services:
162-
security.access.post_voter:
163-
class: AppBundle\Security\Authorization\Voter\PostVoter
164-
public: false
221+
app.post_voter:
222+
class: AppBundle\Security\PostVoter
165223
tags:
166224
- { name: security.voter }
167225
@@ -175,7 +233,7 @@ and tag it with ``security.voter``:
175233
http://symfony.com/schema/dic/services/services-1.0.xsd">
176234
177235
<services>
178-
<service id="security.access.post_voter"
236+
<service id="app.post_voter"
179237
class="AppBundle\Security\Authorization\Voter\PostVoter"
180238
public="false"
181239
>
@@ -190,61 +248,27 @@ and tag it with ``security.voter``:
190248
// app/config/services.php
191249
use Symfony\Component\DependencyInjection\Definition;
192250
193-
$definition = new Definition('AppBundle\Security\Authorization\Voter\PostVoter');
194-
$definition
251+
$container->register('app.post_voter', 'AppBundle\Security\Authorization\Voter\PostVoter')
195252
->setPublic(false)
196253
->addTag('security.voter')
197254
;
198255
199-
$container->setDefinition('security.access.post_voter', $definition);
200-
201-
How to Use the Voter in a Controller
202-
------------------------------------
203-
204-
The registered voter will then always be asked as soon as the method ``isGranted()``
205-
from the authorization checker is called. When extending the base ``Controller``
206-
class, you can simply call the
207-
:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::denyAccessUnlessGranted()`
208-
method::
209-
210-
// src/AppBundle/Controller/PostController.php
211-
namespace AppBundle\Controller;
212-
213-
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
214-
use Symfony\Component\HttpFoundation\Response;
215-
216-
class PostController extends Controller
217-
{
218-
public function showAction($id)
219-
{
220-
// get a Post instance
221-
$post = ...;
222-
223-
// keep in mind that this will call all registered security voters
224-
$this->denyAccessUnlessGranted('view', $post, 'Unauthorized access!');
225-
226-
return new Response('<h1>'.$post->getName().'</h1>');
227-
}
228-
}
229-
230-
.. versionadded:: 2.6
231-
The ``denyAccessUnlessGranted()`` method was introduced in Symfony 2.6.
232-
Prior to Symfony 2.6, you had to call the ``isGranted()`` method of the
233-
``security.context`` service and throw the exception yourself.
234-
235-
It's that easy!
256+
You're done! Now, when you :ref:`call isGranted() with view/edit and a Post object <how-to-use-the-voter-in-a-controller>`,
257+
your voter will be executed and you can control access.
236258

237259
.. _security-voters-change-strategy:
238260

239261
Changing the Access Decision Strategy
240262
-------------------------------------
241263

242-
Imagine you have multiple voters for one action for an object. For instance,
243-
you have one voter that checks if the user is a member of the site and a second
244-
one checking if the user is older than 18.
264+
Normally, only one voter will vote at any given time (the rest will "abstain", which
265+
means they return ``false`` from ``supports()``). But in theory, you could make multiple
266+
voters vote for one action and object. For instance, suppose you have one voter that
267+
checks if the user is a member of the site and a second one that checks if the user
268+
is older than 18.
245269

246270
To handle these cases, the access decision manager uses an access decision
247-
strategy. You can configure this to suite your needs. There are three
271+
strategy. You can configure this to suit your needs. There are three
248272
strategies available:
249273

250274
``affirmative`` (default)

0 commit comments

Comments
 (0)