Este foro ya no está activo, así que no puedes publicar nuevas preguntas ni responder a las preguntas existentes.

Security voter no funciona con SonataAdminBundle

27 de febrero de 2015

Hola.

He tratado de configurar un security voter pero no lo consigo, el atributo al que responde mi voter es ROLE_ADMIN_PRODUCT_VIEW

Este es el service:

services:
    security.access.product_voter:
        class:     AppBundle\Security\Authorization\Voter\ProductVoter
        public:    false
        tags:
            - { name: security.voter }

Esta es la clase ProductVoter

<?php
 
namespace AppBundle\Security\Authorization\Voter;
 
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
 
class ProductVoter implements VoterInterface
{
    public function supportsAttribute($attribute)
    {
        return 'ROLE_ADMIN_PRODUCT_VIEW' == $attribute;
    }
 
    public function supportsClass($class)
    {
        return true;
    }
 
    public function vote(TokenInterface $token, $object, array $attributes)
    {
        foreach ($attributes as $attribute) {
            if (false === $this->supportsAttribute($attribute)) {
                continue;
            }
 
            return VoterInterface::ACCESS_GRANTED;
        }
 
        return VoterInterface::ACCESS_ABSTAIN;
    }
}

Configuración para Sonata:

sonata_admin:
    security:
        handler: sonata.admin.security.handler.role
 
    # ...

Y en el security.yml

security:
    # ...
    access_decision_manager:
        strategy: unanimous

Según la condición if (false === $this->supportsAttribute($attribute)) se cumple, y se retorna return VoterInterface::ACCESS_GRANTED;

Pero aun así cuando accedo al recurso obtengo un Access Denied. Según lo que he visto, otro voter esta denegando el acceso, pero no se cuál.

Me pueden ayudar, no se qué estoy haciendo mal, o qué más debo hacer.

Gracias.


Respuestas

#1

Buenas, hasta donde tengo entendido el handler sonata.admin.security.handler.role se utiliza cuando se tiene definida una estructura de roles como la siguiente:

# app/config/security.yml
security:
    ...
    role_hierarchy:
        ROLE_ADMIN_PRODUCT_READER:
             # Asumiento que el servicio de adiministracion de productos
             # se llama admin.product
            - ROLE_ADMIN_PRODUCT_LIST
            - ROLE_ADMIN_PRODUCT_VIEW

        ROLE_ADMIN: [ROLE_ADMIN_PRODUCT_READER, ROLE_USER]

Con hacer eso, el handler ya debería estar verificando si el usuario tiene el rol ROLE_ADMIN_PRODUCT_VIEW. Esto según la documentación oficial del bundle.

Por otro lado dices que la condición del if de tu Voter se cumple y se retorna VoterInterface::ACCESS_GRANTED, pero realmente si se cumple la condición tienes es un continue; dentro del if. Por lo que más bien parece que siempre se ejecuta el continue y nunca llega al return VoterInterface::ACCESS_GRANTED;.

Saludos!

@manuel_j555

27 febrero 2015, 5:31
#2

Hola @manuel_j555 gracias por tu respuesta.

La instrucción continue lo que hace es terminar esa iteración y continua la otra en caso de que el voter no pueda tomar una decisión, según lo retorne la función supportsAttribute, en definitiva estoy seguro que esta accediendo al VoterInterface::ACCESS_GRANTED.

Por otro lado, estoy viendo lo de la jerarquía de roles y al parecer este es el problema, configuré role_hierarchy de forma correcta y todo funciono bien.

Y quien ofrece el voter que comprueba la herencia de roles es Symfony, no SonataAdmin, puedes verlo en Symfony/Component/Security/Core/Authorization/Voter/RoleHierarchyVoter

Pero a mi parecer sería doble trabajo: por un lado programar el voter (imagina 10 entidades con las acciones CRUD y 6 ROLES diferentes) y por otro siempre configurar la jerarquía de roles.

¿Hay alguna forma de desactivar la comprobación del role_hierarchy? Estaría bien hacerlo? ¿O definitivamente lo mejor seria hacerlo completo?

Gracias

@ramiroanacona

27 febrero 2015, 5:44
#3

Hola, entiendo el funcionamiento del continue, lo más seguro es que sea una confusión mia en la interpretación del comentario del if "Según la condición if (false === $this->supportsAttribute($attribute)) se cumple" que asumia que lo que querías decir es que la condición siempre se cumple :).

Lo del voter que comprueba los roles efectivamente es el RoleVoter de symfony, pero yo hablo del Handler de sonata. Que lo que hace es que en base al nombre del servicio del administrador de la entidad en conjunto con la acción que se quiere verificar (view, list, edit, ...), construye el string que representará al rol.

Así que si por ejemplo el servicio admin se llama app.admin.product y quieres acceder al edit de un producto, el handler creará un string de la forma ROLE_APP_ADMIN_PRODUCT_EDIT, y ese string es el que llegará al RoleVoter, verificando este último que el usuario posea dicho rol en base a la jerarquia de roles definida en la aplicación.

Esto significa que no deberás crear tu un Voter que verifique Roles, ya que symfony tiene uno que lo hace, lo que si deberás hacer es definir la jerarquia de roles por cada entidad creada, lo cual como tu indicas puede ser bastante tedioso cuando son muchas entidades (4 o 5 roles por entidad).

Para desactivar la comprobación de roles y demás está el handler sonata.admin.security.handler.noop de sonata, que siempre devuelve true al llamar al isGranted del admin.

Lo de si es mejor hacerlo completo o no, dependerá de si necesitas que varios tipos de usuario accedan al admin, o solo será de un tipo.

@manuel_j555

27 febrero 2015, 14:26
#4

Hola, gracias por tus comentarios, estamos interpretando las cosas de la misma manera respecto al handler de Sonata y el string que comprobará el handler.

No quiero desactivar todo el handler de sonata, solamente el RoleVoter, de tal manera que con mi Voter yo pueda decidir si el usuario actual tiene acceso a cierto recurso, pero sin necesidad de hacer la herencia de roles, ¿me entiendes?

Es decir que Sonata siga comprobando el acceso a LIST, VIEW, CREATE, etc. pero que Symfony no vote por dicho recurso al comprobar el role del usuario. Después de todo, en mi security voter me encargo de realizar esa comprobación.

Gracias.

@ramiroanacona

27 febrero 2015, 15:01
#5

En una aplicación me tocó crear mi propio handler para lograr más o menos lo que quieres, esto fueron los pasos que seguí:

Se creó el handler, que lo que hace es convertir el llamado de admin.isGranted("VIEW") del sonata en un isGranted("APP_ADMIN_PRODUCT_VIEW") hacia el security de symfony.

namespace AppBundle\Security\Handler;
 
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Security\Handler\SecurityHandlerInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
 
class NativeHandler implements SecurityHandlerInterface
{
    protected $securityContext;
 
    function __construct($securityContext)
    {
        $this->securityContext = $securityContext;
    }
 
    public function isGranted(AdminInterface $admin, $attributes, $object = null)
    {
        if (!is_array($attributes)) {
            $attributes = array($attributes);
        }
 
        foreach ($attributes as $pos => $attribute) {
            if (false === stripos($attribute, 'ROLE_')) {
                //solo si no comienza con ROLE_, concatenamos el id del servicio
                // admin con la acción que se quiere verificar:
                $attributes[$pos] = sprintf($this->getBaseRole($admin), $attribute);
            }
        }
 
        return $this->securityContext->isGranted($attributes, $object);
    }
 
    public function getBaseRole(AdminInterface $admin)
    {
        return str_replace('.', '_', strtoupper($admin->getCode())) . '_%s';
    }
 
    public function buildSecurityInformation(AdminInterface $admin) { }
 
    public function createObjectSecurity(AdminInterface $admin, $object) { }
 
    public function deleteObjectSecurity(AdminInterface $admin, $object) { }
}

Luego creamos el servicio y lo registramos en el sonata:

# app/config/services.yml
services:
    native_security_handler:
        class: AppBundle\Security\Handler\NativeHandler
        arguments: [@security.context]
 
# app/config/config.yml
sonata_admin:
    security:
        handler: native_security_handler

Y luego mis voter son algo como:

class SendMonthVoter extends BaseVoter
{
    public function supportsAttribute($attribute)
    {
        return in_array(strtoupper($attribute), array('SEND', 'APP_ADMIN_MONTH_SEND'));
    }
 
    public function supportsClass($class)
    {
        return $class instanceof Month;
    }
 
    public function vote(TokenInterface $token, $object, array $attributes)
    {
        if (!$this->supportsClass($object)) {
            return self::ACCESS_ABSTAIN;
        }
 
        foreach ($attributes as $attr) {
            if (!$this->supportsAttribute($attr)) {
                continue;
            }
 
            if (!$object->isSend() and $object->getActive()) {
                return self::ACCESS_GRANTED;
            }
 
            return self::ACCESS_DENIED;
        }
 
        return self::ACCESS_ABSTAIN;
    }
}

Espero esto te sirva :) Saludos!

@manuel_j555

27 febrero 2015, 15:20
#6

Hola @manuel_j555 gracias por tus comentarios...

Al parecer esta es la solución, digo al parecer porque hasta ahora funciona bien, voy viendo en el transcurso del desarrollo de la aplicación.

Muchas gracias de nuevo.

@ramiroanacona

27 febrero 2015, 17:01