Symfony2 ACL combined with another criteria Symfony2 ACL combined with another criteria symfony symfony

Symfony2 ACL combined with another criteria


I'm posting this solution so that others can see my final code but here are the pitfalls I found when implementing a Voter as Problematic suggested.

supportsAttribute: It appears that when you call the isGranted method on the SecurityContext that it doesn't actually check this method before delegating a vote call to a VoterInterface so inside your vote method you actually have to check the attributes yourself.

supportsClass: In problematic's answer above it seemed like this method could be a key for a Factory based selection of which VoterInterfaces can vote but actually the symfony2 documentation reads:

The supportsClass() method is used to check if the voter supports the current user token class.

Therefore it actually seems to pertain to whether or not the Voter supports the token type. To make matters worse the PHP Doc seems vague:

Checks if the voter supports the given class.

At any rate the main problem is that this method is never checked by the SecurityContext before delegating the call to the vote method of any voter - even if this method is hardcoded to return false vote will still be called!

So basically the moral of the story appeared to be: check the $attributes and $object coming in on the vote method manually.

My Code:

services.yml

parameters:    comment_voter.class: Acme\Bundle\CommentBundle\Security\Authorization\Voter\CommentVoterservices:    comment_voter:        class: %comment_voter.class%        arguments:  [@service_container]        public: false        tags:          - { name: security.voter }

and the voter class:

<?phpnamespace Acme\Bundle\CommentBundle\Security\Authorization\Voter;use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;use Acme\Bundle\CommentBundle\Entity\Comment;use Symfony\Component\Security\Core\User\UserInterface;/** * A class to check editing privileges for Comments. */class CommentVoter implements VoterInterface {    const AUTHOR_EDIT_TIME_LIMIT    = 300;    private $container;    public function __construct($container) {        $this->container = $container;    }    public function supportsAttribute($attribute) {        return $attribute === 'EDIT';    }    public function supportsClass($class) {        return true;    }    /**     * Checks whether or not the current user can edit a comment.     *      * Users with the role ROLE_COMMENT_MODERATOR may always edit.     * A comment's author can only edit within 5 minutes of it being posted.     *      * {@inheritdoc}     */    public function vote(TokenInterface $token, $object, array $attributes) {        if ( !($object instanceof Comment) ) {            return VoterInterface::ACCESS_ABSTAIN;        }        // Only supports 'EDIT' for now.        if ( !$this->supportsAttribute($attributes[0]) ) {            return VoterInterface::ACCESS_ABSTAIN;        }        $user = $token->getUser();        if ( !($user instanceof UserInterface) ) {            return VoterInterface::ACCESS_DENIED;        }        // Is the token a comment moderator?        if ( $this->container->get('security.context')->isGranted('ROLE_COMMENT_MODERATOR') ) {            return VoterInterface::ACCESS_GRANTED;        }        // Is the token the author of the post and within the edit window.        $originalRevision = $object->getOriginalRevision();        if ( $originalRevision->getAuthor()->equals($user) ) {            if (                 (time() - $originalRevision->getCreationDate()->getTimestamp())                <= self::AUTHOR_EDIT_TIME_LIMIT            ) {                return VoterInterface::ACCESS_GRANTED;            }        }        return VoterInterface::ACCESS_DENIED;    }}

and finally template:

{% if is_granted('EDIT', comment) %}<a href="#">Edit</a>{% endif %}

I hope this helps someone else in the future and a big thanks to Problematic for pointing me in the direction of Voters.


Have you considered using a voter? There's a cookbook recipe for implementing an IP blacklist voter, but it could be easily modified to handle checking for edits on Comment objects.

You can look at the default AclVoter at Symfony\Component\Security\Acl\Voter\AclVoter (online here), though yours can obviously augment instead of replace it and be much simpler.

As a quick proof of concept:

class CommentTimestampVoter implements VoterInterface{    public function supportsAttribute($attribute)    {        return 'edit' === $attribute;    }    public function vote(TokenInterface $token, $object, array $attributes)    {        // 1. check if $token->getUser() has ROLE_ADMIN and return VoterInterface::ACCESS_GRANTED if so        // 2. check if $token->getUser() equals $object->getAuthor() and return VoterInterface::ACCESS_DENIED if not        // 3. check that $object->getCreatedAt() is within the window allowed for editing and return VoterInterface::ACCESS_GRANTED if so        // 4. return VoterInterface::ACCESS_DENIED    }    public function supportsClass($class)    {        return 'Acme\CommentBundle\Entity\Comment' === $class;    }}