Skip to content

Voter update #5908

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 4 commits into from
Nov 30, 2015
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Reworking the voter article for the new Voter class
  • Loading branch information
weaverryan committed Nov 27, 2015
commit 20cead68da3dff710815da31a5301130c5bba28f
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::
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

version number is missing here

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why changing the namespace to use AppBundle\Security instead of AppBundle\Security\Authorization\Voter ?

If the application grows, it could have many things in the Security namespace. I think the cookbooks are good indications for a new developper to know where to place common things and promote some standard directory structures.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree partially with @ogizanagi. Why not using AppBundle\Security\Voter as the namespace?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AppBundle\Security\Voter is fine to me too. 👍
I only have for habit to replicate the subnamespace of the component used for such things.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the best practice is to not clutter the AppBundle\Form with different sub-namespaces I'd say let's be consistent with that decision and do not introduce sub-namespaces here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @xabbuh. There is no perfect answer, but I do hate telling people to create one directory, then another directory inside that directory before doing something.


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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We must check if $subject is an object before using instanceof.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to have that check, fortunately :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, actually @wouterj already proved me wrong. :)

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to check if $user is an object.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should avoid using "we" here

/** @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!');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The service should still be private.

}
}

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

boolean

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``
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra backquote before $subject

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)``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a label for this headline.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks :) - there is a headline now, but it's further up on the page

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')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

- $container->register('app.post_voter', 'AppBundle\Security\Authorization\Voter\PostVoter')
+ $container->register('app.post_voter', 'AppBundle\Security\PostVoter')

if you decide to keep it in the AppBundle\Security namespace. Same for Xml.
Also, public: false is missing from yaml.

->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
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy