Buenas prácticas oficiales de Symfony

9.5. Los Security Voters

Si la lógica relacionada con la seguridad es compleja y no se puede centralizar en un método de la entidad, como el anterior método isAuthor(), entonces deberías utilizar un security voter propio. La gran ventaja de los voters respecto a las ACL es que son un orden de magnitud más sencillos de crear y en casi todos los casos ofrecen la misma flexibilidad.

Para utilizar este mecanismo, crea primero la clase del voter. El siguiente ejemplo muestra la case utilizada en la aplicación para el voter basado en el método getAuthorEmail:

namespace AppBundle\Security;

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

// La clase AbstractVoter está disponible a partir de Symfony 2.6
class PostVoter extends AbstractVoter
{
    const CREATE = 'create';
    const EDIT   = 'edit';

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

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

    protected function isGranted($attribute, $post, $user = null)
    {
        if (!$user instanceof UserInterface) {
            return false;
        }

        if ($attribute == self::CREATE && in_array(ROLE_ADMIN, $user->getRoles())) {
            return true;
        }

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

        return false;
    }
}

Para activar el voter en la aplicación, define un nuevo servicio y etiquétalo con la etiqueta security.voter:

# app/config/services.yml
services:
    # ...
    post_voter:
        class:      AppBundle\Security\PostVoter
        public:     false
        tags:
           - { name: security.voter }

Lo bueno del voter es que también se puede utilizar junto con la anotación @Security:

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("is_granted('edit', post)")
 */
public function editAction(Post $post)
{
    // ...
}

Y por supuesto también se puede combinar con el servicio security.context mediante el método isGranted():

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 */
public function editAction($id)
{
    $post = // query for the post ...

    if (!$this->get('security.context')->isGranted('edit', $post)) {
        throw $this->createAccessDeniedException();
    }
}