Skip to content

Commit eb2f7bd

Browse files
committed
feature #5423 [Security] add & update doc entries on AbstractVoter implementation (Inoryy, javiereguiluz)
This PR was submitted for the 2.7 branch but it was merged into the 2.6 branch instead (closes #5423). Discussion ---------- [Security] add & update doc entries on AbstractVoter implementation | Q | A | ------------- | --- | Doc fix? | no | New docs? | yes (symfony/symfony#11183) | Applies to | 2.6+ | Fixed tickets | - This PR finishes #4257. Commits ------- 95537d3 Added a link to the AbstractVoter cookbook 73bd908 Fixed some typos 60643f0 Removed the abstract_voter.rst.inc file 59c60b1 add fixes to abstract_voter include file e9053c0 fix problems pointed out by @javiereguiluz and @cordoval 968cb65 add & update doc entries on AbstractVoter implementation
2 parents ae27dd1 + 95537d3 commit eb2f7bd

File tree

2 files changed

+70
-80
lines changed

2 files changed

+70
-80
lines changed

cookbook/security/voters.rst

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ the security layer. This can be done easily through the service container.
9292
methods in your implementation of the ``vote()`` method and return ``ACCESS_ABSTAIN``
9393
if your voter does not support the class or attribute.
9494

95+
.. tip::
96+
97+
An :doc:`AbstractVoter </cookbook/security/voters_data_permission>` is
98+
provided to cover the common use cases when implementing security voters.
99+
95100
Declaring the Voter as a Service
96101
--------------------------------
97102

@@ -197,8 +202,8 @@ application configuration file with the following code.
197202
That's it! Now, when deciding whether or not a user should have access,
198203
the new voter will deny access to any user in the list of blacklisted IPs.
199204

200-
Note that the voters are only called, if any access is actually checked. So
201-
you need at least something like
205+
Note that the voters are only called, if any access is actually checked. So
206+
you need at least something like
202207

203208
.. configuration-block::
204209

cookbook/security/voters_data_permission.rst

Lines changed: 63 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ How Symfony Uses Voters
2525

2626
In order to use voters, you have to understand how Symfony works with them.
2727
All voters are called each time you use the ``isGranted()`` method on Symfony's
28-
authorization checker (i.e. the ``security.authorization_checker`` service). Each
28+
authorization checker (i.e. the ``security.authorization_checker`` service). Each
2929
one decides if the current user should have access to some resource.
3030

3131
Ultimately, Symfony uses one of three different approaches on what to do
@@ -37,11 +37,19 @@ For more information take a look at
3737
The Voter Interface
3838
-------------------
3939

40-
A custom voter must implement
41-
:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`,
42-
which has this structure:
40+
A custom voter needs to implement
41+
:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`
42+
or extend :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter`,
43+
which makes creating a voter even easier.
4344

44-
.. include:: /cookbook/security/voter_interface.rst.inc
45+
.. code-block:: php
46+
47+
abstract class AbstractVoter implements VoterInterface
48+
{
49+
abstract protected function getSupportedClasses();
50+
abstract protected function getSupportedAttributes();
51+
abstract protected function isGranted($attribute, $object, $user = null);
52+
}
4553
4654
In this example, the voter will check if the user has access to a specific
4755
object according to your custom conditions (e.g. they must be the owner of
@@ -61,90 +69,74 @@ edit a particular object. Here's an example implementation:
6169
// src/AppBundle/Security/Authorization/Voter/PostVoter.php
6270
namespace AppBundle\Security\Authorization\Voter;
6371
64-
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
65-
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
72+
use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter;
6673
use Symfony\Component\Security\Core\User\UserInterface;
6774
68-
class PostVoter implements VoterInterface
75+
class PostVoter extends AbstractVoter
6976
{
7077
const VIEW = 'view';
7178
const EDIT = 'edit';
7279
73-
public function supportsAttribute($attribute)
80+
protected function getSupportedAttributes()
7481
{
75-
return in_array($attribute, array(
76-
self::VIEW,
77-
self::EDIT,
78-
));
82+
return array(self::VIEW, self::EDIT);
7983
}
8084
81-
public function supportsClass($class)
85+
protected function getSupportedClasses()
8286
{
83-
$supportedClass = 'AppBundle\Entity\Post';
84-
85-
return $supportedClass === $class || is_subclass_of($class, $supportedClass);
87+
return array('AppBundle\Entity\Post');
8688
}
8789
88-
/**
89-
* @var \AppBundle\Entity\Post $post
90-
*/
91-
public function vote(TokenInterface $token, $post, array $attributes)
90+
protected function isGranted($attribute, $post, $user = null)
9291
{
93-
// check if class of this object is supported by this voter
94-
if (!$this->supportsClass(get_class($post))) {
95-
return VoterInterface::ACCESS_ABSTAIN;
96-
}
97-
98-
// check if the voter is used correct, only allow one attribute
99-
// this isn't a requirement, it's just one easy way for you to
100-
// design your voter
101-
if (1 !== count($attributes)) {
102-
throw new \InvalidArgumentException(
103-
'Only one attribute is allowed for VIEW or EDIT'
104-
);
105-
}
106-
107-
// set the attribute to check against
108-
$attribute = $attributes[0];
109-
110-
// check if the given attribute is covered by this voter
111-
if (!$this->supportsAttribute($attribute)) {
112-
return VoterInterface::ACCESS_ABSTAIN;
113-
}
114-
115-
// get current logged in user
116-
$user = $token->getUser();
117-
11892
// make sure there is a user object (i.e. that the user is logged in)
11993
if (!$user instanceof UserInterface) {
120-
return VoterInterface::ACCESS_DENIED;
94+
return false;
95+
}
96+
97+
// the data object could have for example a method isPrivate()
98+
// which checks the Boolean attribute $private
99+
if ($attribute == self::VIEW && !$post->isPrivate()) {
100+
return true;
121101
}
122102
123-
switch($attribute) {
124-
case self::VIEW:
125-
// the data object could have for example a method isPrivate()
126-
// which checks the boolean attribute $private
127-
if (!$post->isPrivate()) {
128-
return VoterInterface::ACCESS_GRANTED;
129-
}
130-
break;
131-
132-
case self::EDIT:
133-
// we assume that our data object has a method getOwner() to
134-
// get the current owner user entity for this data object
135-
if ($user->getId() === $post->getOwner()->getId()) {
136-
return VoterInterface::ACCESS_GRANTED;
137-
}
138-
break;
103+
// we assume that our data object has a method getOwner() to
104+
// get the current owner user entity for this data object
105+
if ($attribute == self::EDIT && $user->getId() === $post->getOwner()->getId()) {
106+
return true;
139107
}
140108
141-
return VoterInterface::ACCESS_DENIED;
109+
return false;
142110
}
143111
}
144112
145113
That's it! The voter is done. The next step is to inject the voter into
146114
the security layer.
147115

116+
To recap, here's what's expected from the three abstract methods:
117+
118+
:method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter::getSupportedClasses`
119+
It tells Symfony that your voter should be called whenever an object of one
120+
of the given classes is passed to ``isGranted()``. For example, if you return
121+
``array('AppBundle\Model\Product')``, Symfony will call your voter when a
122+
``Product`` object is passed to ``isGranted()``.
123+
124+
:method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter::getSupportedAttributes`
125+
It tells Symfony that your voter should be called whenever one of these
126+
strings is passed as the first argument to ``isGranted()``. For example, if
127+
you return ``array('CREATE', 'READ')``, then Symfony will call your voter
128+
when one of these is passed to ``isGranted()``.
129+
130+
:method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\AbstractVoter::isGranted`
131+
It implements the business logic that verifies whether or not a given user is
132+
allowed access to a given attribute (e.g. ``CREATE`` or ``READ``) on a given
133+
object. This method must return a boolean.
134+
135+
.. note::
136+
137+
Currently, to use the ``AbstractVoter`` base class, you must be creating a
138+
voter where an object is always passed to ``isGranted()``.
139+
148140
Declaring the Voter as a Service
149141
--------------------------------
150142

@@ -203,6 +195,7 @@ from the authorization checker is called.
203195
204196
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
205197
use Symfony\Component\HttpFoundation\Response;
198+
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
206199
207200
class PostController extends Controller
208201
{
@@ -212,25 +205,17 @@ from the authorization checker is called.
212205
$post = ...;
213206
214207
// keep in mind, this will call all registered security voters
215-
$this->denyAccessUnlessGranted('view', $post, 'Unauthorized access!');
216-
217-
// the equivalent code without using the denyAccessUnlessGranted() shortcut
218-
// use Symfony\Component\Security\Core\Exception\AccessDeniedException;
219-
//
220-
// if (false === $this->get('security.authorization_checker')->isGranted('view', $post)) {
221-
// throw new AccessDeniedException('Unauthorized access!');
222-
// }
208+
if (false === $this->get('security.authorization_checker')->isGranted('view', $post)) {
209+
throw new AccessDeniedException('Unauthorised access!');
210+
}
223211
224212
return new Response('<h1>'.$post->getName().'</h1>');
225213
}
226214
}
227215
228216
.. versionadded:: 2.6
229-
The ``security.authorization_checker`` service was introduced in Symfony 2.6. Prior
230-
to Symfony 2.6, you had to use the ``isGranted()`` method of the ``security.context`` service.
231-
232-
.. versionadded:: 2.6
233-
The ``denyAccessUnlessGranted()`` method was introduced in Symfony 2.6 as a shortcut.
234-
It uses ``security.authorization_checker`` and throws an ``AccessDeniedException`` if needed.
217+
The ``security.authorization_checker`` service was introduced in Symfony 2.6.
218+
Prior to Symfony 2.6, you had to use the ``isGranted()`` method of the
219+
``security.context`` service.
235220

236221
It's that easy!

0 commit comments

Comments
 (0)