domingo, 3 de noviembre de 2013

¿Cómo implementar un "Voter" en Symfony2?

Recientemente me encontré con la siguiente necesidad para la implementación de seguridades en Symfony2.

Si un usuario es ROLE_SITEADMIN, debe poder acceder y modificar todo.
Si un usuario es ROLE_USER, entonces, se deben seguir las reglas ACE (de las ACL).

La idea es emplear una única comprobación.
Esto significa, que a un usuario ROLE_SITEADMIN, el isGranted, lo siguiente debería devolver TRUE.
---- // código en el controlador 
if (false === $this->get('security.context')->isGranted('EDIT', $entity))
----

y evitar el tener que hacer doble chequeo en cada uno de los sitios que aparece:
---
if (false === $this->get('security.context')->isGranted('EDIT', $entity) && !$this->get('security.context')->isGranted("ROLE_SITEADMIN"))
---

¿Hay alguna forma de hacerlo sin tener que reimplementar toda la clase de las ACL?

Pues para eso están los Voter.

Veamoslo con un ejemplo:
------------------
<?php
// src/Acme/Bundle/CommonBundle/Security/Authorization/Voter;

namespace Acme\Bundle\CommonBundle\Security\Authorization\Voter;

use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpKernel\Log\LoggerInterface;

class SiteAdminVoter implements VoterInterface
{
    var $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger        = $logger;
    }

    public function supportsAttribute($attribute)
    {
        // you won't check against a user attribute, so return true
        return true;
    }

    public function supportsClass($class)
    {
        // your voter supports all type of token classes, so return true
        return true;
    }

    public function vote(TokenInterface $token, $object, array $attributes)
    {
        $result = VoterInterface::ACCESS_ABSTAIN;
        $user = $token->getUser();
        if ("anon." === $user) {
            return VoterInterface::ACCESS_ABSTAIN;
        }
        if ($user->hasRole('ROLE_SITEADMIN')) {
                $this->logger->info("SiteAdminVoter: user is Admin, Access Granted");
                return VoterInterface::ACCESS_GRANTED;
        }
        return VoterInterface::ACCESS_ABSTAIN;
    }
}


?>
-------------

Con esto, se crea una clase que puede garantizar acceso, en función de lo que queramos. En este caso, de si el usuario posee el ROLE_SITEADMIN.

Existe una estrategia que se configura en el config.yml que indica si:
* Basta con que un "voter" permita acceso para garantizarlo (y por defecto, si todos los "voter" se abtienen, entonces se deniega)
o * Basta con que un "voter" deniegue el acceso para denegarlo 

Yo lo he dejado por defecto. Así que es una puerta más para abrir los permisos.

Así que estoy dando permisos a mis objetos a los dueños (y a lo que ajusto mediante las ACL), y también mediante el ROLE. Un ROLE_SITEADMIN, puede modificar todo. Así que el isGranted('EDIT')  debe devolver "true" o bien porque tengas el permiso correspondiente en la ACL, o por ser un ROLE_SITEADMIN.

Sólo queda configurar el "Voter" para que se llame:
services:
    subscription_voter:
        class: Acme\Bundle\CommonBundle\Security\Authorization\Voter\SiteAdminVoter
        public: false
        arguments:  ["@monolog.logger"]
        tags:
            - { name: security.voter }

Con el tag, es cómo lo registramos entre los "voter".

--- esta entrada ha sido actualizada gracias a la aportación de Thiago Brito  para inyectar exclusivamente el logger en vez de todo el container ---