@@ -35,120 +35,179 @@ The Voter Interface
35
35
36
36
A custom voter needs to implement
37
37
: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 `,
39
39
which makes creating a voter even easier.
40
40
41
41
.. code-block :: php
42
42
43
- abstract class AbstractVoter implements VoterInterface
43
+ abstract class Voter implements VoterInterface
44
44
{
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);
48
47
}
49
48
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.
56
106
57
107
Creating the custom Voter
58
108
-------------------------
59
109
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::
62
114
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;
67
117
68
- use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter ;
118
+ use AppBundle\Entity\Post ;
69
119
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;
71
122
72
- class PostVoter extends AbstractVoter
123
+ class PostVoter extends Voter
73
124
{
125
+ // these strings are just invented: you can use anything
74
126
const VIEW = 'view';
75
127
const EDIT = 'edit';
76
128
77
- protected function getSupportedAttributes( )
129
+ protected function supports($attribute, $subject )
78
130
{
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
+ }
81
135
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;
85
142
}
86
143
87
- protected function isGranted ($attribute, $post, $user = null )
144
+ protected function voteOnAttribute ($attribute, $subject, TokenInterface $token )
88
145
{
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();
93
147
94
- // double-check that the User object is the expected entity (this
95
- // only happens when you did not configure the security system properly)
96
148
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;
98
151
}
99
152
153
+ // we know $subject is a Post object, thanks to supports
154
+ /** @var Post $post */
155
+ $post = $subject;
156
+
100
157
switch($attribute) {
101
158
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);
109
160
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);
117
162
}
118
163
119
- return false ;
164
+ throw new \LogicException('This code should not be reached!') ;
120
165
}
121
- }
122
166
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
+ }
125
178
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
+ }
127
186
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 >`.
133
188
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:
139
190
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.
144
200
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.
146
206
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 :
149
208
150
- Declaring the Voter as a Service
151
- --------------------------------
209
+ Configuring the Voter
210
+ ---------------------
152
211
153
212
To inject the voter into the security layer, you must declare it as a service
154
213
and tag it with ``security.voter ``:
@@ -159,9 +218,8 @@ and tag it with ``security.voter``:
159
218
160
219
# app/config/services.yml
161
220
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
165
223
tags :
166
224
- { name: security.voter }
167
225
@@ -175,7 +233,7 @@ and tag it with ``security.voter``:
175
233
http://symfony.com/schema/dic/services/services-1.0.xsd" >
176
234
177
235
<services >
178
- <service id =" security.access .post_voter"
236
+ <service id =" app .post_voter"
179
237
class =" AppBundle\Security\Authorization\Voter\PostVoter"
180
238
public =" false"
181
239
>
@@ -190,61 +248,27 @@ and tag it with ``security.voter``:
190
248
// app/config/services.php
191
249
use Symfony\Component\DependencyInjection\Definition;
192
250
193
- $definition = new Definition('AppBundle\Security\Authorization\Voter\PostVoter');
194
- $definition
251
+ $container->register('app.post_voter', 'AppBundle\Security\Authorization\Voter\PostVoter')
195
252
->setPublic(false)
196
253
->addTag('security.voter')
197
254
;
198
255
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.
236
258
237
259
.. _security-voters-change-strategy :
238
260
239
261
Changing the Access Decision Strategy
240
262
-------------------------------------
241
263
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.
245
269
246
270
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
248
272
strategies available:
249
273
250
274
``affirmative `` (default)
0 commit comments