New in Symfony 2.6: Simpler Security Voters

Contributed by
Roman Marintšenko and Ryan Weaver in #11183.

Security Voters provide a mechanism to set up fine-grained restrictions in Symfony applications. The main advantage over ACLs is that they are an order of magnitude easier to set up, configure and use.

In previous Symfony versions, voters implemented the VoterInterface interface, which has the following signature:

1
2
3
4
5
6
interface VoterInterface
{
    public function supportsAttribute($attribute);
    public function supportsClass($class);
    public function vote(TokenInterface $token, $object, array $attributes);
}

Implementing this interface is pretty easy, but the resulting code was usually a bit bloated, as demonstrated by the following 83 lines of code needed to define a simple voter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// src/Acme/DemoBundle/Security/Authorization/Voter/PostVoter.php
namespace Acme\DemoBundle\Security\Authorization\Voter;

use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;

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($class)
    {
        $supportedClass = 'Acme\DemoBundle\Entity\Post';

        return $supportedClass === $class || is_subclass_of($class, $supportedClass);
    }

    /**
     * @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(get_class($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];

        // check if the given attribute is covered by this voter
        if (!$this->supportsAttribute($attribute)) {
            return VoterInterface::ACCESS_ABSTAIN;
        }

        // get current logged in user
        $user = $token->getUser();

        // 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 self::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 self::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;
        }

        return VoterInterface::ACCESS_DENIED;
    }
}

As the result of the Symfony DX initiative, Symfony 2.6 will allow to define much simpler security voters. To do so, use the new AbstractVoter class which implements VoterInterface and defines the following methods:

1
2
3
4
5
6
7
8
9
abstract class AbstractVoter implements VoterInterface
{
    public function supportsAttribute($attribute);
    public function supportsClass($class);
    public function vote(TokenInterface $token, $object, array $attributes);
    abstract protected function getSupportedClasses();
    abstract protected function getSupportedAttributes();
    abstract protected function isGranted($attribute, $object, $user = null);
}

The three methods supportsAttribute(), supportsClass() and vote() help you reduce the boilerplate code of the voter and let you focus on the specific business logic of your application. As a result, the same voter shown above now takes only 41 lines of code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// src/Acme/DemoBundle/Security/Authorization/Voter/PostVoter.php
namespace Acme\DemoBundle\Security\Authorization\Voter;

use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter;
use Symfony\Component\Security\Core\User\UserInterface;

class PostVoter extends AbstractVoter
{
    const VIEW = 'view';
    const EDIT = 'edit';

    protected function getSupportedAttributes()
    {
        return array(self::VIEW, self::EDIT);
    }

    protected function getSupportedClasses()
    {
        return array('Acme\DemoBundle\Entity\Post');
    }

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

        // custom business logic to decide if the given user can view
        // and/or edit the given post
        if ($attribute == self::VIEW && !$post->isPrivate()) {
            return true;
        }

        if ($attribute == self::EDIT && $user->getId() === $post->getOwner()->getId()) {
            return true;
        }

        return false;
    }
}

Writing less code to get the same results as before boosts your productivity. That's our obsession since the introduction of the DX initiative and Symfony 2.6 will be the first version to embrace this new philosophy.

Comments

Nice work.

+1000 points for the DX initiative.
Hey, thats really nice.
Git One question: what about putting buisness Logic into the decission whether an item IS allowed to be shown in a list i.e an List of items in an admin? I won't ever do a isGranted() in a foreach loop. But is there a mechanism planed?
In my current project i implemented a mechanism very near to the Voters. By the help of SonataAdmin i got access to the query that creates the list. So build a Factory which loops tagged "queryHelpers" which have an equal interface as Voters. But instead a vote they just reduce/manipulate the the query.
I know this method would need some more abstraction, but i would open a RFC if somebody is interested to see more Details.
I think what you want is a doctrine SQL filter ( in combination with security.context)
Exactly. And as i think it is an authorisation problem, it should be part of Symfony Security, as a Kind of abstraction for sure.
I don't quite understand the need of supportsAttribute and supportsClass methods in the interface at all. At least in symfony core, they are used only privately in the voters or in AccessDecisionManagerInterface. And the same methods from this manager are not used anywhere in the core.

As these methods are not called by the manager before voting, you must duplicate (or at least write additional) code in the vote method itself.

And if you do not support the attribute or object, you just return VoterInterface::ACCESS_ABSTAIN from the vote method. Furthermore, sometimes class is not enough to decide - you need the object itself.

To conclude, I think this abstract voter could have had default implementations of these methods, as they are not actually called in Symfony standard.

Comments are closed.

To ensure that comments stay relevant, they are closed for old posts.