Skip to content

Commit

Permalink
feature #3594 New Data Voter Article (continuation) (weaverryan)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
weaverryan committed Mar 4, 2014
2 parents adcbb5d + d3f9383 commit b9608a7
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 23 deletions.
1 change: 1 addition & 0 deletions cookbook/map.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
* :doc:`/cookbook/security/remember_me`
* :doc:`/cookbook/security/impersonating_user`
* :doc:`/cookbook/security/voters`
* :doc:`/cookbook/security/voters_data_permission`
* :doc:`/cookbook/security/acl`
* :doc:`/cookbook/security/acl_advanced`
* :doc:`/cookbook/security/force_https`
Expand Down
2 changes: 1 addition & 1 deletion cookbook/security/acl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ the ACL system comes in.
Using ACL's isn't trivial, and for simpler use cases, it may be overkill.
If your permission logic could be described by just writing some code (e.g.
to check if a Blog is owned by the current User), then consider using
:doc:`voters </cookbook/security/voters>`. A voter is passed the object
:doc:`voters </cookbook/security/voters_data_permission>`. A voter is passed the object
being voted on, which you can use to make complex decisions and effectively
implement your own ACL. Enforcing authorization (e.g. the ``isGranted``
part) will look similar to what you see in this entry, but your voter
Expand Down
1 change: 1 addition & 0 deletions cookbook/security/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Security
remember_me
impersonating_user
voters
voters_data_permission
acl
acl_advanced
force_https
Expand Down
24 changes: 24 additions & 0 deletions cookbook/security/voter_interface.rst.inc
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.
23 changes: 1 addition & 22 deletions cookbook/security/voters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,7 @@ A custom voter must implement
:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`,
which requires the following three methods:

.. code-block:: php
interface VoterInterface
{
public function supportsAttribute($attribute);
public function supportsClass($class);
public function vote(TokenInterface $token, $object, array $attributes);
}
The ``supportsAttribute()`` method is used to check if the voter supports
the given user attribute (i.e: a role, an ACL, etc.).

The ``supportsClass()`` method is used to check if the voter supports the
class of the object whose access is being checked (doesn't apply to this entry).

The ``vote()`` method must implement the business logic that verifies whether
or not the user is granted 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.
.. include:: /cookbook/security/voter_interface.rst.inc

In this example, you'll check if the user's IP address matches against a list of
blacklisted addresses and "something" will be the application. If the user's IP is blacklisted, you'll return
Expand Down
225 changes: 225 additions & 0 deletions cookbook/security/voters_data_permission.rst
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!

0 comments on commit b9608a7

Please sign in to comment.