8000 feature #3594 New Data Voter Article (continuation) (weaverryan) · symfony/symfony-docs@b9608a7 · GitHub
[go: up one dir, main page]

Skip to content

Commit b9608a7

Browse files
committed
feature #3594 New Data Voter Article (continuation) (weaverryan)
This PR was merged into the 2.3 branch. Discussion ---------- New Data Voter Article (continuation) Hiya guys! This is just a continuation of #3138 by @monbro. I needed to rebase it against the 2.3 branch (which had conflicts), and wanted to proofread it (hence the fresh PR). Please check out my latest commit to see if I've made any mistakes :). | Q | A | ------------- | --- | Doc fix? | no | New docs? | yes | Applies to | 2.3 | Fixed tickets | #2877 Thanks! Commits ------- d3f9383 [#3594] Nice tweaks thanks to @wouterj and @xabbuh 2391758 [#2877][#3138] Proofreading the new voter data permission entry 11aead7 simplified the example 8227270 updated according to the review da7b97e missed one comment 9b91501 updated the docs according to the last review 872a05f updated the link from ACL to the data permission voters 1fd3b0e updated docs according to the review 5275230 updated with missing fixes f4eb5f3 updated docs according to the reviews ee0def1 improved tip box with additional link to /cookbook/security 731dcad updated page with suggestion from the review 1466fa7 improvements according to the reviews 99b1b0f a couple of changes according to the comments, not finished now 2bda150 create voters_data_permission.rst article
2 parents adcbb5d + d3f9383 commit b9608a7

File tree

6 files changed

+253
-23
lines changed

6 files changed

+253
-23
lines changed

cookbook/map.rst.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
* :doc:`/cookbook/security/remember_me`
128128
* :doc:`/cookbook/security/impersonating_user`
129129
* :doc:`/cookbook/security/voters`
130+
* :doc:`/cookbook/security/voters_data_permission`
130131
* :doc:`/cookbook/security/acl`
131132
* :doc:`/cookbook/security/acl_advanced`
132133
* :doc:`/cookbook/security/force_https`

cookbook/security/acl.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ the ACL system comes in.
1414
Using ACL's isn't trivial, and for simpler use cases, it may be overkill.
1515
If your permission logic could be described by just writing some code (e.g.
1616
to check if a Blog is owned by the current User), then consider using
17-
:doc:`voters </cookbook/security/voters>`. A voter is passed the object
17+
:doc:`voters </cookbook/security/voters_data_permission>`. A voter is passed the object
1818
being voted on, which you can use to make complex decisions and effectively
1919
implement your own ACL. Enforcing authorization (e.g. the ``isGranted``
2020
part) will look similar to what you see in this entry, but your voter

cookbook/security/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Security
88
remember_me
99
impersonating_user
1010
voters
11+
voters_data_permission
1112
acl
1213
acl_advanced
1314
force_https
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.. code-block:: php
2+
3+
interface VoterInterface
4+
{
5+
public function supportsAttribute($attribute);
6+
public function supportsClass($class);
7+
public function vote(TokenInterface $token, $post, array $attributes);
8+
}
9+
10+
The :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::supportsAttribute`
11+
method is used to check if the voter supports the given user attribute (i.e:
12+
a role like ``ROLE_USER``, an ACL ``EDIT``, etc.).
13+
14+
The :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::supportsClass`
15+
method is used to check if the voter supports the class of the object whose
16+
access is being checked.
17+
18+
The :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::vote`
19+
method must implement the business logic that verifies whether or not the
20+
user has access. This method must return one of the following values:
21+
22+
* ``VoterInterface::ACCESS_GRANTED``: The authorization will be granted by this voter;
23+
* ``VoterInterface::ACCESS_ABSTAIN``: The voter cannot decide if authorization should be granted;
24+
* ``VoterInterface::ACCESS_DENIED``: The authorization will be denied by this voter.

cookbook/security/voters.rst

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,7 @@ A custom voter must implement
2121
:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`,
2222
which requires the following three methods:
2323

24-
.. code-block:: php
25-
26-
interface VoterInterface
27-
{
28-
public function supportsAttribute($attribute);
29-
public function supportsClass($class);
30-
public function vote(TokenInterface $token, $object, array $attributes);
31-
}
32-
33-
The ``supportsAttribute()`` method is used to check if the voter supports
34-
the given user attribute (i.e: a role, an ACL, etc.).
35-
36-
The ``supportsClass()`` method is used to check if the voter supports the
37-
class of the object whose access is being checked (doesn't apply to this entry).
38-
39-
The ``vote()`` method must implement the business logic that verifies whether
40-
or not the user is granted access. This method must return one of the following
41-
values:
42-
43-
* ``VoterInterface::ACCESS_GRANTED``: The authorization will be granted by this voter;
44-
* ``VoterInterface::ACCESS_ABSTAIN``: The voter cannot decide if authorization should be granted;
45-
* ``VoterInterface::ACCESS_DENIED``: The authorization will be denied by this voter.
24+
.. include:: /cookbook/security/voter_interface.rst.inc
4625

4726
In this example, you'll check if the user's IP address matches against a list of
4827
blacklisted addresses and "something" will be the application. If the user's IP is blacklisted, you'll return
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
.. index::
2+
single: Security; Data Permission Voters
3+
4+
How to Use Voters to Check User Permissions
5+
===========================================
6+
7+
In Symfony2 you can check the permission to access data by using the
8+
:doc:`ACL module </cookbook/security/acl>`, which is a bit overwhelming
9+
for many applications. A much easier solution is to work with custom voters,
10+
which are like simple conditional statements.
11+
12+
.. seealso::
13+
14+
Voters can also be used in other ways, like, for example, blacklisting IP
15+
addresses from the entire application: :doc:`/cookbook/security/voters`.
16+
17+
.. tip::
18+
19+
Take a look at the
20+
:doc:`authorization </components/security/authorization>`
21+
chapter for an even deeper understanding on voters.
22+
23+
How Symfony Uses Voters
24+
-----------------------
25+
26+
In order to use voters, you have to understand how Symfony works with them.
27+
All voters are called each time you use the ``isGranted()`` method on Symfony's
28+
security context (i.e. the ``security.context`` service). Each one decides
29+
if the current user should have access to some resource.
30+
31+
Ultimately, Symfony uses one of three different approaches on what to do
32+
with the feedback from all voters: affirmative, consensus and unanimous.
33+
34+
For more information take a look at
35+
:ref:`the section about access decision managers <components-security-access-decision-manager>`.
36+
37+
The Voter Interface
38+
-------------------
39+
40+
A custom voter must implement
41+
:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`,
42+
which has this structure:
43+
44+
.. include:: /cookbook/security/voter_interface.rst.inc
45+
46+
In this example, the voter will check if the user has access to a specific
47+
object according to your custom conditions (e.g. they must be the owner of
48+
the object). If the condition fails, you'll return
49+
``VoterInterface::ACCESS_DENIED``, otherwise you'll return
50+
``VoterInterface::ACCESS_GRANTED``. In case the responsibility for this decision
51+
does not belong to this voter, it will return ``VoterInterface::ACCESS_ABSTAIN``.
52+
53+
Creating the Custom Voter
54+
-------------------------
55+
56+
The goal is to create a voter that checks if a user has access to view or
57+
edit a particular object. Here's an example implementation:
58+
59+
// src/Acme/DemoBundle/Security/Authorization/Voter/PostVoter.php
60+
namespace Acme\DemoBundle\Security\Authorization\Voter;
61+
62+
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
63+
use Symfony\Component\DependencyInjection\ContainerInterface;
64+
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
65+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
66+
use Symfony\Component\Security\Core\User\UserInterface;
67+
use Acme\DemoBundle\Entity\Post;
68+
69+
class PostVoter implements VoterInterface
70+
{
71+
const VIEW = 'view';
72+
const EDIT = 'edit';
73+
74+
public function supportsAttribute($attribute)
75+
{
76+
return in_array($attribute, array(
77+
self::VIEW,
78+
self::EDIT,
79+
));
80+
}
81+
82+
public function supportsClass($obj)
83+
{
84+
return $obj instanceof Post;
85+
}
86+
87+
/**
88+
* @var \Acme\DemoBundle\Entity\Post $post
89+
*/
90+
public function vote(TokenInterface $token, $post, array $attributes)
91+
{
92+
// check if class of this object is supported by this voter
93+
if (!$this->supportsClass($post)) {
94+
return VoterInterface::ACCESS_ABSTAIN;
95+
}
96+
97+
// check if the voter is used correct, only allow one attribute
98+
// this isn't a requirement, it's just one easy way for you to
99+
// design your voter
100+
if(1 !== count($attributes)) {
101+
throw new InvalidArgumentException(
102+
'Only one attribute is allowed for VIEW or EDIT'
103+
);
104+
}
105+
106+
// set the attribute to check against
107+
$attribute = $attributes[0];
108+
109+
// get current logged in user
110+
$user = $token->getUser();
111+
112+
// check if the given attribute is covered by this voter
113+
if (!$this->supportsAttribute($attribute)) {
114+
return VoterInterface::ACCESS_ABSTAIN;
115+
}
116+
117+
// make sure there is a user object (i.e. that the user is logged in)
118+
if (!$user instanceof UserInterface) {
119+
return VoterInterface::ACCESS_DENIED;
120+
}
121+
122+
switch($attribute) {
123+
case 'view':
124+
// the data object could have for example a method isPrivate()
125+
// which checks the Boolean attribute $private
126+
if (!$post->isPrivate()) {
127+
return VoterInterface::ACCESS_GRANTED;
128+
}
129+
break;
130+
131+
case 'edit':
132+
// we assume that our data object has a method getOwner() to
133+
// get the current owner user entity for this data object
134+
if ($user->getId() === $post->getOwner()->getId()) {
135+
return VoterInterface::ACCESS_GRANTED;
136+
}
137+
break;
138+
}
139+
}
140+
}
141+
142+
That's it! The voter is done. The next step is to inject the voter into
143+
the security layer.
144+
145+
Declaring the Voter as a Service
146+
--------------------------------
147+
148+
To inject the voter into the security layer, you must declare it as a service
149+
and tag it with ``security.voter``:
150+
151+
.. configuration-block::
152+
153+
.. code-block:: yaml
154+
155+
# src/Acme/DemoBundle/Resources/config/services.yml
156+
services:
157+
security.access.post_voter:
158+
class: Acme\DemoBundle\Security\Authorization\Voter\PostVoter
159+
public: false
160+
tags:
161+
- { name: security.voter }
162+
163+
.. code-block:: xml
164+
165+
<!-- src/Acme/DemoBundle/Resources/config/services.xml -->
166+
<?xml version="1.0" encoding="UTF-8" ?>
167+
<container xmlns="http://symfony.com/schema/dic/services"
168+
xsi:schemaLocation="http://symfony.com/schema/dic/services
169+
http://symfony.com/schema/dic/services/services-1.0.xsd">
170+
<services>
171+
<service id="security.access.post_document_voter"
172+
class="Acme\DemoBundle\Security\Authorization\Voter\PostVoter"
173+
public="false">
174+
<tag name="security.voter" />
175+
</service>
176+
</services>
177+
</container>
178+
179+
.. code-block:: php
180+
181+
// src/Acme/DemoBundle/Resources/config/services.php
182+
$container
183+
->register(
184+
'security.access.post_document_voter',
185+
'Acme\DemoBundle\Security\Authorization\Voter\PostVoter'
186+
)
187+
->addTag('security.voter')
188+
;
189+
190+
How to Use the Voter in a Controller
191+
------------------------------------
192+
193+
The registered voter will then always be asked as soon as the method ``isGranted()``
194+
from the security context is called.
195+
196+
.. code-block:: php
197+
198+
// src/Acme/DemoBundle/Controller/PostController.php
199+
namespace Acme\DemoBundle\Controller;
200+
201+
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
202+
use Symfony\Component\HttpFoundation\Response;
203+
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
204+
205+
class PostController extends Controller
206+
{
207+
public function showAction()
208+
{
209+
// get a Post instance
210+
$post = ...;
211+
212+
// keep in mind, this will call all registered security voters
213+
if (false === $this->get('security.context')->isGranted('view', $post)) {
214+
throw new AccessDeniedException('Unauthorised access!');
215+
}
216+
217+
$product = $this->getDoctrine()
218+
->getRepository('AcmeStoreBundle:Post')
219+
->find($id);
220+
221+
return new Response('<h1>'.$post->getName().'</h1>');
222+
}
223+
}
224+
225+
It's that easy!

0 commit comments

Comments
 (0)
0