-
-
Notifications
You must be signed in to change notification settings - Fork 5.2k
New Data Voter Article (continuation) #3594
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
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
2bda150
create voters_data_permission.rst article
99b1b0f
a couple of changes according to the comments, not finished now
1466fa7
improvements according to the reviews
731dcad
updated page with suggestion from the review
ee0def1
improved tip box with additional link to /cookbook/security
f4eb5f3
updated docs according to the reviews
5275230
updated with missing fixes
1fd3b0e
updated docs according to the review
872a05f
updated the link from ACL to the data permission voters
9b91501
updated the docs according to the last review
da7b97e
missed one comment
8227270
updated according to the review
11aead7
simplified the example
2391758
[#2877][#3138] Proofreading the new voter data permission entry
weaverryan d3f9383
[#3594] Nice tweaks thanks to @WouterJ and @xabbuh
weaverryan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ Security | |
remember_me | ||
impersonating_user | ||
voters | ||
voters_data_permission | ||
acl | ||
acl_advanced | ||
force_https | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
.. code-block:: php | ||
|
||
interface VoterInterface | ||
{ | ||
public function supportsAttribute($attribute); | ||
public function supportsClass($class); | ||
public function vote(TokenInterface $token, $post, array $attributes); | ||
} | ||
|
||
The :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::supportsAttribute` | ||
method is used to check if the voter supports the given user attribute (i.e: | ||
a role like ``ROLE_USER``, an ACL ``EDIT``, etc.). | ||
|
||
The :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::supportsClass` | ||
method is used to check if the voter supports the class of the object whose | ||
access is being checked. | ||
|
||
The :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::vote` | ||
method must implement the business logic that verifies whether or not the | ||
user has access. This method must return one of the following values: | ||
|
||
* ``VoterInterface::ACCESS_GRANTED``: The authorization will be granted by this voter; | ||
* ``VoterInterface::ACCESS_ABSTAIN``: The voter cannot decide if authorization should be granted; | ||
* ``VoterInterface::ACCESS_DENIED``: The authorization will be denied by this voter. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
.. index:: | ||
single: Security; Data Permission Voters | ||
|
||
How to Use Voters to Check User Permissions | ||
=========================================== | ||
|
||
In Symfony2 you can check the permission to access data by using the | ||
:doc:`ACL module </cookbook/security/acl>`, which is a bit overwhelming | ||
for many applications. A much easier solution is to work with custom voters, | ||
which are like simple conditional statements. | ||
|
||
.. seealso:: | ||
|
||
Voters can also be used in other ways, like, for example, blacklisting IP | ||
addresses from the entire application: :doc:`/cookbook/security/voters`. | ||
|
||
.. tip:: | ||
|
||
Take a look at the | ||
:doc:`authorization </components/security/authorization>` | ||
chapter for an even deeper understanding on voters. | ||
|
||
How Symfony Uses Voters | ||
----------------------- | ||
|
||
In order to use voters, you have to understand how Symfony works with them. | ||
All voters are called each time you use the ``isGranted()`` method on Symfony's | ||
security context (i.e. the ``security.context`` service). Each one decides | ||
if the current user should have access to some resource. | ||
|
||
Ultimately, Symfony uses one of three different approaches on what to do | ||
with the feedback from all voters: affirmative, consensus and unanimous. | ||
|
||
For more information take a look at | ||
:ref:`the section about access decision managers <components-security-access-decision-manager>`. | ||
|
||
The Voter Interface | ||
------------------- | ||
|
||
A custom voter must implement | ||
:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`, | ||
which has this structure: | ||
|
||
.. include:: /cookbook/security/voter_interface.rst.inc | ||
|
||
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``. | ||
|
||
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: | ||
|
||
// src/Acme/DemoBundle/Security/Authorization/Voter/PostVoter.php | ||
namespace Acme\DemoBundle\Security\Authorization\Voter; | ||
|
||
use Symfony\Component\Security\Core\Exception\InvalidArgumentException; | ||
use Symfony\Component\DependencyInjection\ContainerInterface; | ||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; | ||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||
use Symfony\Component\Security\Core\User\UserInterface; | ||
use Acme\DemoBundle\Entity\Post; | ||
|
||
class PostVoter implements VoterInterface | ||
{ | ||
const VIEW = 'view'; | ||
const EDIT = 'edit'; | ||
|
||
public function supportsAttribute($attribute) | ||
{ | ||
return in_array($attribute, array( | ||
self::VIEW, | ||
self::EDIT, | ||
)); | ||
} | ||
|
||
public function supportsClass($obj) | ||
{ | ||
return $obj instanceof Post; | ||
} | ||
|
||
/** | ||
* @var \Acme\DemoBundle\Entity\Post $post | ||
*/ | ||
public function vote(TokenInterface $token, $post, array $attributes) | ||
{ | ||
// check if class of this object is supported by this voter | ||
if (!$this->supportsClass($post)) { | ||
return VoterInterface::ACCESS_ABSTAIN; | ||
} | ||
|
||
// check if the voter is used correct, only allow one attribute | ||
// this isn't a requirement, it's just one easy way for you to | ||
// design your voter | ||
if(1 !== count($attributes)) { | ||
throw new InvalidArgumentException( | ||
'Only one attribute is allowed for VIEW or EDIT' | ||
); | ||
} | ||
|
||
// set the attribute to check against | ||
$attribute = $attributes[0]; | ||
|
||
// get current logged in user | ||
$user = $token->getUser(); | ||
|
||
// check if the given attribute is covered by this voter | ||
if (!$this->supportsAttribute($attribute)) { | ||
return VoterInterface::ACCESS_ABSTAIN; | ||
} | ||
|
||
// make sure there is a user object (i.e. that the user is logged in) | ||
if (!$user instanceof UserInterface) { | ||
return VoterInterface::ACCESS_DENIED; | ||
} | ||
|
||
switch($attribute) { | ||
case 'view': | ||
// the data object could have for example a method isPrivate() | ||
// which checks the Boolean attribute $private | ||
if (!$post->isPrivate()) { | ||
return VoterInterface::ACCESS_GRANTED; | ||
} | ||
break; | ||
|
||
case 'edit': | ||
// we assume that our data object has a method getOwner() to | ||
// get the current owner user entity for this data object | ||
if ($user->getId() === $post->getOwner()->getId()) { | ||
return VoterInterface::ACCESS_GRANTED; | ||
} | ||
break; | ||
} | ||
} | ||
} | ||
|
||
That's it! The voter is done. The next step is to inject the voter into | ||
the security layer. | ||
|
||
Declaring the Voter as a Service | ||
-------------------------------- | ||
|
||
To inject the voter into the security layer, you must declare it as a service | ||
and tag it with ``security.voter``: | ||
|
||
.. configuration-block:: | ||
|
||
.. code-block:: yaml | ||
|
||
# src/Acme/DemoBundle/Resources/config/services.yml | ||
services: | ||
security.access.post_voter: | ||
class: Acme\DemoBundle\Security\Authorization\Voter\PostVoter | ||
public: false | ||
tags: | ||
- { name: security.voter } | ||
|
||
.. code-block:: xml | ||
|
||
<!-- src/Acme/DemoBundle/Resources/config/services.xml --> | ||
<?xml version="1.0" encoding="UTF-8" ?> | ||
<container xmlns="http://symfony.com/schema/dic/services" | ||
xsi:schemaLocation="http://symfony.com/schema/dic/services | ||
http://symfony.com/schema/dic/services/services-1.0.xsd"> | ||
<services> | ||
<service id="security.access.post_document_voter" | ||
class="Acme\DemoBundle\Security\Authorization\Voter\PostVoter" | ||
public="false"> | ||
<tag name="security.voter" /> | ||
</service> | ||
</services> | ||
</container> | ||
|
||
.. code-block:: php | ||
|
||
// src/Acme/DemoBundle/Resources/config/services.php | ||
$container | ||
->register( | ||
'security.access.post_document_voter', | ||
'Acme\DemoBundle\Security\Authorization\Voter\PostVoter' | ||
) | ||
->addTag('security.voter') | ||
; | ||
|
||
How to Use the Voter in a Controller | ||
------------------------------------ | ||
|
||
The registered voter will then always be asked as soon as the method ``isGranted()`` | ||
from the security context is called. | ||
|
||
.. code-block:: php | ||
|
||
// src/Acme/DemoBundle/Controller/PostController.php | ||
namespace Acme\DemoBundle\Controller; | ||
|
||
use Symfony\Bundle\FrameworkBundle\Controller\Controller; | ||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\Security\Core\Exception\AccessDeniedException; | ||
|
||
class PostController extends Controller | ||
{ | ||
public function showAction() | ||
{ | ||
// get a Post instance | ||
$post = ...; | ||
|
||
// keep in mind, this will call all registered security voters | ||
if (false === $this->get('security.context')->isGranted('view', $post)) { | ||
throw new AccessDeniedException('Unauthorised access!'); | ||
} | ||
|
||
$product = $this->getDoctrine() | ||
->getRepository('AcmeStoreBundle:Post') | ||
->find($id); | ||
|
||
return new Response('<h1>'.$post->getName().'</h1>'); | ||
} | ||
} | ||
|
||
It's that easy! | ||
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 love this "happy ryan" addition 😉 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
"like, for example" aren't that 2 words saying the same? I would use either "like" or "for example".
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.
I like it the way it is.