Skip to content

Commit

Permalink
Reworking the voter article for the new Voter class
Browse files Browse the repository at this point in the history
  • Loading branch information
weaverryan committed Nov 27, 2015
1 parent 3ebf2d0 commit 20cead6
Showing 1 changed file with 143 additions and 119 deletions.
262 changes: 143 additions & 119 deletions cookbook/security/voters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,120 +35,179 @@ The Voter Interface

A custom voter needs to implement
:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`
or extend :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter`,
or extend :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\Voter`,
which makes creating a voter even easier.

.. code-block:: php
abstract class AbstractVoter implements VoterInterface
abstract class Voter implements VoterInterface
{
abstract protected function getSupportedClasses();
abstract protected function getSupportedAttributes();
abstract protected function isGranted($attribute, $object, $user = null);
abstract protected function supports($attribute, $subject);
abstract protected function voteOnAttribute($attribute, $subject, TokenInterface $token);
}
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``.
.. versionadded::
The ``Voter`` helper class was added in Symfony 2.8. In early versions, an
``AbstractVoter`` class with similar behavior was available.

.. _how-to-use-the-voter-in-a-controller:

Setup: Checking for Access in a Controller
------------------------------------------

Suppose you have a ``Post`` object and you need to decide whether or not the current
user can *edit* or *view* the object. In your controller, you'll check access with
code like this::

// src/AppBundle/Controller/PostController.php
// ...

class PostController extends Controller
{
/**
* @Route("/posts/{id}", name="post_show")
*/
public function showAction($id)
{
// get a Post object - e.g. query for it
$post = ...;

// check for "view" access: calls all voters
$this->denyAccessUnlessGranted('view', $post);

// ...
}

/**
* @Route("/posts/{id}/edit", name="post_edit")
*/
public function editAction($id)
{
// get a Post object - e.g. query for it
$post = ...;

// check for "edit" access: calls all voters
$this->denyAccessUnlessGranted('edit', $post);

// ...
}
}

The ``denyAccessUnlessGranted()`` method (and also, the simpler ``isGranted()`` method)
calls out to the "voter" system. Right now, no voters will vote on whether or not
the user can "view" or "edit" a ``Post``. But you can create your *own* voter that
decides this using whatever logic you want.

.. tip::

The ``denyAccessUnlessGranted()`` function and the ``isGranted()`` functions
are both just shortcuts to call ``isGranted()`` on the ``security.authorization_checker``
service.

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:
Suppose the logic to decide if a user can "view" or "edit" a ``Post`` object is
pretty complex. For example, a ``User`` can always edit or view a ``Post`` they created.
And if a ``Post`` is marked as "public", anyone can view it. A voter for this situation
would look like this::

.. code-block:: php
// src/AppBundle/Security/Authorization/Voter/PostVoter.php
namespace AppBundle\Security\Authorization\Voter;
// src/AppBundle/Security/PostVoter.php
namespace AppBundle\Security;

use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter;
use AppBundle\Entity\Post;
use AppBundle\Entity\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends AbstractVoter
class PostVoter extends Voter
{
// these strings are just invented: you can use anything
const VIEW = 'view';
const EDIT = 'edit';

protected function getSupportedAttributes()
protected function supports($attribute, $subject)
{
return array(self::VIEW, self::EDIT);
}
// if the attribute isn't one we support, return false
if (!in_array($attribute, array(self::VIEW, self::EDIT))) {
return false;
}

protected function getSupportedClasses()
{
return array('AppBundle\Entity\Post');
// only vote on Post objects inside this voter
if (!$subject instanceof Post) {
return false;
}

return true;
}

protected function isGranted($attribute, $post, $user = null)
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
// make sure there is a user object (i.e. that the user is logged in)
if (!$user instanceof UserInterface) {
return false;
}
$user = $token->getUser();

// double-check that the User object is the expected entity (this
// only happens when you did not configure the security system properly)
if (!$user instanceof User) {
throw new \LogicException('The user is somehow not our User class!');
// the user must not be logged in, so we deny access
return false;
}

// we know $subject is a Post object, thanks to supports
/** @var Post $post */
$post = $subject;

switch($attribute) {
case self::VIEW:
// the data object could have for example a method isPrivate()
// which checks the Boolean attribute $private
if (!$post->isPrivate()) {
return true;
}
break;
return $this->canView($post, $user);
case self::EDIT:
// this assumes that the data object has a getOwner() method
// to get the entity of the user who owns this data object
if ($user->getId() === $post->getOwner()->getId()) {
return true;
}
break;
return $this->canEdit($post, $user);
}

return false;
throw new \LogicException('This code should not be reached!');
}
}

That's it! The voter is done. The next step is to inject the voter into
the security layer.
private function canView(Post $post, User $user)
{
// if they can edit, they can view
if ($this->canEdit($post, $user)) {
return true;
}

// the Post object could have, for example, a method isPrivate()
// that checks a Boolean $private property
return !$post->isPrivate();
}

To recap, here's what's expected from the three abstract methods:
private function canEdit(Post $post, User $user)
{
// this assumes that the data object has a getOwner() method
// to get the entity of the user who owns this data object
return $user === $post->getOwner();
}
}

:method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter::getSupportedClasses`
It tells Symfony that your voter should be called whenever an object of one
of the given classes is passed to ``isGranted()``. For example, if you return
``array('AppBundle\Model\Product')``, Symfony will call your voter when a
``Product`` object is passed to ``isGranted()``.
That's it! The voter is done! Next, :ref:`configure it <declaring-the-voter-as-a-service>`.

:method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter::getSupportedAttributes`
It tells Symfony that your voter should be called whenever one of these
strings is passed as the first argument to ``isGranted()``. For example, if
you return ``array('CREATE', 'READ')``, then Symfony will call your voter
when one of these is passed to ``isGranted()``.
To recap, here's what's expected from the two abstract methods:

:method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter::isGranted`
It implements the business logic that verifies whether or not a given user is
allowed access to a given attribute (e.g. ``CREATE`` or ``READ``) on a given
object. This method must return a boolean.
``Voter::supports($attribute, $subject)``
When ``isGranted()`` (or ``denyAccessUnlessGranted()``) is called, the first
argument is passed here as ``$attribute`` (e.g. ``ROLE_USER``, ``edit``) and
the second argument (if any) is passed as ```$subject`` (e.g. ``null``, a ``Post``
object). Your job is to determine if your voter should vote on the attribute/subject
combination. If you return true, ``voteOnAttribute()`` will be called. Otherwise,
your voter is done: some other voter should process this. In this example, you
return ``true`` if the attribue is ``view`` or ``edit`` and if the object is
a ``Post`` instance.

.. note::
``voteOnAttribute($attribute, $subject, TokenInterface $token)``
If you return ``true`` from ``supports()``, then this method is called. Your
job is simple: return ``true`` to allow access and ``false`` to deny access.
The ``$token`` can be used to find the current user object (if any). In this
example, all of the complex business logic is included to determine access.

Currently, to use the ``AbstractVoter`` base class, you must be creating a
voter where an object is always passed to ``isGranted()``.
.. _declaring-the-voter-as-a-service:

Declaring the Voter as a Service
--------------------------------
Configuring the Voter
---------------------

To inject the voter into the security layer, you must declare it as a service
and tag it with ``security.voter``:
Expand All @@ -159,9 +218,8 @@ and tag it with ``security.voter``:
# app/config/services.yml
services:
security.access.post_voter:
class: AppBundle\Security\Authorization\Voter\PostVoter
public: false
app.post_voter:
class: AppBundle\Security\PostVoter
tags:
- { name: security.voter }
Expand All @@ -175,7 +233,7 @@ and tag it with ``security.voter``:
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="security.access.post_voter"
<service id="app.post_voter"
class="AppBundle\Security\Authorization\Voter\PostVoter"
public="false"
>
Expand All @@ -190,61 +248,27 @@ and tag it with ``security.voter``:
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
$definition = new Definition('AppBundle\Security\Authorization\Voter\PostVoter');
$definition
$container->register('app.post_voter', 'AppBundle\Security\Authorization\Voter\PostVoter')
->setPublic(false)
->addTag('security.voter')
;
$container->setDefinition('security.access.post_voter', $definition);
How to Use the Voter in a Controller
------------------------------------

The registered voter will then always be asked as soon as the method ``isGranted()``
from the authorization checker is called. When extending the base ``Controller``
class, you can simply call the
:method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::denyAccessUnlessGranted()`
method::

// src/AppBundle/Controller/PostController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class PostController extends Controller
{
public function showAction($id)
{
// get a Post instance
$post = ...;

// keep in mind that this will call all registered security voters
$this->denyAccessUnlessGranted('view', $post, 'Unauthorized access!');

return new Response('<h1>'.$post->getName().'</h1>');
}
}

.. versionadded:: 2.6
The ``denyAccessUnlessGranted()`` method was introduced in Symfony 2.6.
Prior to Symfony 2.6, you had to call the ``isGranted()`` method of the
``security.context`` service and throw the exception yourself.

It's that easy!
You're done! Now, when you :ref:`call isGranted() with view/edit and a Post object <how-to-use-the-voter-in-a-controller>`,
your voter will be executed and you can control access.

.. _security-voters-change-strategy:

Changing the Access Decision Strategy
-------------------------------------

Imagine you have multiple voters for one action for an object. For instance,
you have one voter that checks if the user is a member of the site and a second
one checking if the user is older than 18.
Normally, only one voter will vote at any given time (the rest will "abstain", which
means they return ``false`` from ``supports()``). But in theory, you could make multiple
voters vote for one action and object. For instance, suppose you have one voter that
checks if the user is a member of the site and a second one that checks if the user
is older than 18.

To handle these cases, the access decision manager uses an access decision
strategy. You can configure this to suite your needs. There are three
strategy. You can configure this to suit your needs. There are three
strategies available:

``affirmative`` (default)
Expand Down

0 comments on commit 20cead6

Please sign in to comment.