Cómo implementar tu propio votante para agregar direcciones IP a la lista negra

El componente Security de Symfony2 ofrece varias capas para autenticar a los usuarios. Una de las capas se llama voter («votante» en adelante). Un votante es una clase dedicada que comprueba si el usuario tiene el derecho a conectarse con la aplicación. Por ejemplo, Symfony2 proporciona una capa que comprueba si el usuario está plenamente autenticado o si se espera que tenga algún rol.

A veces es útil crear un votante personalizado para tratar un caso específico que la plataforma no maneja. En esta sección, aprenderás cómo crear un votante que te permitirá añadir usuarios a la lista negra por su IP.

La interfaz Votante

Un votante personalizado debe implementar la clase Symfony\Component\Security\Core\Authorization\Voter\VoterInterface, la cual requiere los tres siguientes métodos:

interface VoterInterface
{
    function supportsAttribute($attribute);
    function supportsClass($class);
    function vote(TokenInterface $token, $object, array $attributes);
}

El método supportsAttribute() se utiliza para comprobar si el votante admite el atributo usuario dado (es decir: un rol, una acl, etc.).

El método supportsClass() se utiliza para comprobar si el votante apoya a la clase simbólica usuario actual.

El método vote() debe implementar la lógica del negocio que verifica cuando o no se concede acceso al usuario. Este método debe devolver uno de los siguientes valores:

  • VoterInterface::ACCESS_GRANTED: El usuario ahora puede acceder a la aplicación
  • VoterInterface::ACCESS_ABSTAIN: El votante no puede decidir si permitir al usuario o no
  • VoterInterface::ACCESS_DENIED: El usuario no tiene permitido el acceso a la aplicación

En este ejemplo, debes comprobar si la dirección IP del usuario coincide con una lista de direcciones en la lista negra. Si la IP del usuario está en la lista negra, devuelves VoterInterface::ACCESS_DENIED, de lo contrario devuelves VoterInterface::ACCESS_ABSTAIN porque la finalidad del votante sólo es denegar el acceso, no permitir el acceso.

Creando un votante personalizado

Para poner a un usuario en la lista negra basándote en su IP, puedes utilizar el servicio request y comparar la dirección IP contra un conjunto de direcciones IP en la lista negra:

// src/Acme/DemoBundle/Security/Authorization/Voter/ClientIpVoter.php
namespace Acme\DemoBundle\Security\Authorization\Voter;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class ClientIpVoter implements VoterInterface
{
    public function __construct(ContainerInterface $container, array $blacklistedIp = array())
    {
        $this->container     = $container;
        $this->blacklistedIp = $blacklistedIp;
    }

    public function supportsAttribute($attribute)
    {
        // no vas a verificar contra un atributo de usuario, por tanto devuelve 'true'
        return true;
    }

    public function supportsClass($class)
    {
        // tu votante apoya todo tipo de clases, por tanto devuelve 'true'
        return true;
    }

    function vote(TokenInterface $token, $object, array $attributes)
    {
        $request = $this->container->get('request');
        if (in_array($request->getClientIp(), $this->blacklistedIp)) {
            return VoterInterface::ACCESS_DENIED;
        }

        return VoterInterface::ACCESS_ABSTAIN;
    }
}

¡Eso es todo! El votante está listo. El siguiente paso es inyectar el votante en el nivel de seguridad. Esto se puede hacer fácilmente a través del contenedor de servicios.

Declarando el votante como servicio

Para inyectar al votante en la capa de seguridad, lo debes declarar como servicio, y etiquetarlo como «security.voter»:

  • YAML
    # src/Acme/AcmeBundle/Resources/config/services.yml
    services:
        security.access.blacklist_voter:
            class:      Acme\DemoBundle\Security\Authorization\Voter\ClientIpVoter
            arguments:  ["@service_container", [123.123.123.123, 171.171.171.171]]
            public:     false
            tags:
                    - { name: security.voter }
    
  • XML
    <!-- src/Acme/AcmeBundle/Resources/config/services.xml -->
    <service id="security.access.blacklist_voter"
             class="Acme\DemoBundle\Security\Authorization\Voter\ClientIpVoter" public="false">
        <argument type="service" id="service_container" strict="false" />
        <argument type="collection">
            <argument>123.123.123.123</argument>
            <argument>171.171.171.171</argument>
        </argument>
        <tag name="security.voter" />
    </service>
    
  • PHP
    // src/Acme/AcmeBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    use Symfony\Component\DependencyInjection\Reference;
    
    $definition = new Definition(
        'Acme\DemoBundle\Security\Authorization\Voter\ClientIpVoter',
        array(
            new Reference('service_container'),
            array('123.123.123.123', '171.171.171.171'),
        ),
    );
    $definition->addTag('security.voter');
    $definition->setPublic(false);
    
    $container->setDefinition('security.access.blacklist_voter', $definition);
    

Truco

Asegúrate de importar este archivo de configuración desde el archivo de configuración principal de tu aplicación (por ejemplo, app/config/config.yml). Para más información, consulta Importando configuración con imports. Para leer más acerca de definir los servicios en general, consulta el capítulo Contenedor de servicios.

Cambiando la estrategia de decisión de acceso

A fin de que los cambios del nuevo votante tengan efecto, tienes que cambiar la estrategia de decisión de acceso predeterminada, la cual por omisión, concede el acceso si cualquier votante permite el acceso.

En este caso, escoge la estrategia unanimous. A diferencia de la estrategia afirmativa (predeterminada), con la estrategia unanimous, aunque un votante sólo niega el acceso (p. ej. el ClientIpVoter), no otorga acceso al usuario final.

Para ello, sustituye la sección predeterminada access_decision_manager del archivo de configuración de tu aplicación con el siguiente código.

  • YAML
    # app/config/security.yml
    security:
        access_decision_manager:
            # la estrategia puede ser: affirmative, unanimous o consensus
            strategy: unanimous
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <!-- La estrategia pueder ser: affirmative, unanimous o consensus -->
        <access-decision-manager strategy="unanimous">
    </config>
    
  • PHP
    // app/config/security.xml
    $container->loadFromExtension('security', array(
        // la estrategia puede ser: affirmative, unanimous o consensus
        'access_decision_manager' => array(
            'strategy' => 'unanimous',
        ),
    ));
    

¡Eso es todo! Ahora, a la hora de decidir si un usuario debe tener acceso o no, el nuevo votante niega el acceso a cualquier usuario en la lista negra de direcciones IP.

Bifúrcame en GitHub