Skip to content

Commit 9dcf804

Browse files
committed
Reworking the voter article for the new Voter class
1 parent 329182d commit 9dcf804

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