Symfony 2.0, el libro oficial

13.4. Autorización

La autenticación siempre es el primer paso en la seguridad, ya que se trata del proceso de verificar quién es el usuario. En Symfony, la autenticación se puede hacer de varias maneras: a través de un formulario de acceso, con la autenticación básica de HTTP, e incluso a través de Twitter o Facebook.

Una vez que el usuario se ha autenticado, comienza la autorización, que proporciona un mecanismo estándar y muy potente para decidir si un usuario puede acceder a algún recurso (una URL, un objeto, una llamada a un método, ...). Su funcionamiento se basa en asignar roles específicos a cada usuario y después hacer que las diferentes partes de la aplicación requieran de diferentes roles para poder acceder.

El proceso de autorización tiene por tanto dos componentes principales:

  1. El usuario dispones de uno o más roles.
  2. Un recurso requiere de un rol específico para poder acceder a él..

En esta sección, nos centraremos en cómo utilizar diferentes roles para proteger el acceso a los recursos. Más adelante, aprenderás más sobre cómo crear y asignar roles a los usuarios.

13.4.1.  Protegiendo URL específicas mediante patrones

La forma más sencilla de proteger parte de tu aplicación consiste en utilizar expresiones regulares para proteger URL específicas. En el primer ejemplo de este capítulo ya se configuró la autorización para que cualquier URL que coincida con la expresión regular ^/admin requiera el rol ROLE_ADMIN.

Advertencia Entender cómo trabaja access_control exactamente es muy importante para garantizar que tu aplicación esté completamente asegurada. Más adelante se explica su funcionamiento con detalle.

Puedes definir tantos patrones de URL como necesites, cada uno de ellos con una expresión regular:

# app/config/security.yml
security:
    # ...
    access_control:
        - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
        - { path: ^/admin, roles: ROLE_ADMIN }
<!-- app/config/security.xml -->
<config>
    <!-- ... -->
    <rule path="^/admin/users" role="ROLE_SUPER_ADMIN" />
    <rule path="^/admin" role="ROLE_ADMIN" />
</config>
// app/config/security.php
$container->loadFromExtension('security', array(
    // ...
    'access_control' => array(
        array('path' => '^/admin/users', 'role' => 'ROLE_SUPER_ADMIN'),
        array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
    ),
));

Truco Al prefijar la ruta con ^ te aseguras que sólo coinciden las URL que comienzan con ese patrón. Si utilizaras por ejemplo la ruta /admin (sin el ^ por delante) se aplicaría tanto a rutas del tipo /admin/foo como a rutas de este otro tipo: /foo/admin.

13.4.2. Entendiendo cómo trabaja access_control

Symfony2 comprueba para cada petición entrante si existe algún access_control cuya expresión regular coincida con la URL de la petición. En cuanto encuentra un access_control que coincide, detiene la búsqueda, ya que siempre se utiliza el primer access_control coincidente (así que el orden en el que configures los diferentes access_control es muy importante).

Cada access_control tiene varias opciones de configuración que se encargan de dos tareas:

  • Comprobar si la petición coincide con los datos del access_control
  • En caso de que coincida, qué tipo de control de acceso debe realizarse.

Para comprobar si la petición coincide con el access_control, Symfony2 crea una instancia de la clase RequestMatcher por cada uno de los access_control. Esta clase determina si este access_control debe aplicarse a la petición. Para realizar esta comprobación, se utilizan las siguientes opciones:

  • path
  • ip
  • host
  • methods

Considera las siguientes entradas de access_control como ejemplo:

# app/config/security.yml
security:
    # ...
    access_control:
        - { path: ^/admin, roles: ROLE_USER_IP, ip: 127.0.0.1 }
        - { path: ^/admin, roles: ROLE_USER_HOST, host: symfony.com }
        - { path: ^/admin, roles: ROLE_USER_METHOD, methods: [POST, PUT] }
        - { path: ^/admin, roles: ROLE_USER }
<access-control>
    <rule path="^/admin" role="ROLE_USER_IP" ip="127.0.0.1" />
    <rule path="^/admin" role="ROLE_USER_HOST" host="symfony.com" />
    <rule path="^/admin" role="ROLE_USER_METHOD" method="POST, PUT" />
    <rule path="^/admin" role="ROLE_USER" />
</access-control>
'access_control' => array(
    array('path' => '^/admin', 'role' => 'ROLE_USER_IP', 'ip' => '127.0.0.1'),
    array('path' => '^/admin', 'role' => 'ROLE_USER_HOST', 'host' => 'symfony.com'),
    array('path' => '^/admin', 'role' => 'ROLE_USER_METHOD', 'method' => 'POST, PUT'),
    array('path' => '^/admin', 'role' => 'ROLE_USER'),
),

Para cada petición entrante, Symfony decide qué access_control utilizar basándose en la URI solicitada, la dirección IP del cliente, el nombre del servidor o host, y el método de la petición. Si para una entrada no se especifican las opciones ip, host o method, ese access_control se aplicará a cualquier dirección IP, host y método. Por último, recuerda que el orden es muy importante porque Symfony siempre utiliza la primera coincidencia:

Ejemplo #1:

  • Datos de la petición entrante:
    • URI: /admin/user
    • IP: 127.0.0.1
    • Host: example.com
    • Method: GET
  • access_control utilizado por Symfony:
    • Regla #1 (ROLE_USER_IP)
  • ¿Por qué?:
    • Porque la URI coincide con path y la IP con ip.

Ejemplo #2:

  • Datos de la petición entrante:
    • URI: /admin/user
    • IP: 127.0.0.1
    • Host: symfony.com
    • Method: GET
  • access_control utilizado por Symfony:
    • Regla #1 (ROLE_USER_IP)
  • ¿Por qué?:
    • Porque también coinciden los valores de path e ip. También se produce una coincidencia con la entrada del rol ROLE_USER_HOST, pero Symfony siempre utiliza la primera regla que coincida.

Ejemplo #3:

  • Datos de la petición entrante:
    • URI: /admin/user
    • IP: 168.0.0.1
    • Host: symfony.com
    • Method: GET
  • access_control utilizado por Symfony:
    • Regla #2 (ROLE_USER_HOST)
  • ¿Por qué?:
    • La dirección IP no concuerda con la de la primera regla, por tanto se utiliza la segunda regla (cuya configuración sí que coincide con la petición).

Ejemplo #4:

  • Datos de la petición entrante:
    • URI: /admin/user
    • IP: 168.0.0.1
    • Host: symfony.com
    • Method: POST
  • access_control utilizado por Symfony:
    • Regla #2 (ROLE_USER_HOST)
  • ¿Por qué?:
    • Los datos de la segunda regla siguen siendo válidos para esta petición. La tercera regla también se cumple, pero Symfony siempre utiliza dirección IP no concuerda con la de la primera regla, por tanto se utiliza la primera regla que coincida.

Ejemplo #5:

  • Datos de la petición entrante:
    • URI: /admin/user
    • IP: 168.0.0.1
    • Host: example.com
    • Method: POST
  • access_control utilizado por Symfony:
    • Regla #3 (ROLE_USER_METHOD)
  • ¿Por qué?:
    • Los datos de la petición no coinciden ni con la primera ni con la segunda regla. La tercera regla sí que coincide y por eso es la que se utiliza.

Ejemplo #6:

  • Datos de la petición entrante:
    • URI: /admin/user
    • IP: 168.0.0.1
    • Host: example.com
    • Method: GET
  • access_control utilizado por Symfony:
    • Regla #4 (ROLE_USER)
  • ¿Por qué?:
    • Los valores de ip, host y method impiden que alguna de las tres primeras reglas coincida.

Ejemplo #7:

  • Datos de la petición entrante:
    • URI: /foo
    • IP: 127.0.0.1
    • Host: symfony.com
    • Method: POST
  • access_control utilizado por Symfony:
    • Ninguna.
  • ¿Por qué?:
    • La URI solicitada no coincide con la expresión regular de ninguna regla.

Una vez que Symfony2 ha decidido que entrada access_control concuerda con la petición (si es que hay alguna), entonces aplica las restricciones de acceso basándose en las opciones roles y requires_channel:

  • role: si el usuario no tiene del rol indicado, se le deniega el acceso (internamente se lanza la excepción AccessDeniedException).
  • requires_channel: si el canal de la petición (por ejemplo http) no coincide con el valor de esta opción (por ejemplo https), el usuario será redirigido (en este caso se le redirige de http a https).

Truco Si se deniega el acceso al usuario, el sistema de seguridad intentará autenticar al usuario si aún no lo ha hecho (redirigiéndole por ejemplo a la página del formulario de login). Si el usuario ya había iniciado sesión, se le muestra la página del error 403 (Access denied).

13.4.3. Protegiendo por IP

En algunas situaciones puede ser útil restringir el acceso a una determinada ruta basándose en la IP desde la que se origina la petición. Esto es especialmente importante si utilizas ESI en tu aplicación (que es algo que está relacionado con la caché HTTP y se explicará más adelante en otro capítulo).

Cuándo ESI está habilitado, es recomendable proteger el acceso a las URL de ESI. De hecho, algunos fragmentos ESI pueden contener información tan sensible como todos los datos del usuario actualmente conectado en la aplicación. Para impedir cualquier posibilidad de acceder a estos fragmentos, la ruta ESI debe asegurarse de forma que solamente sea visible desde algún servidor privado.

El siguiente ejemplo muestra cómo proteger todas las rutas ESI que empiezan con un determinado prefijo (/esi) para que no se accedan desde el exterior:

# app/config/security.yml
security:
    # ...
    access_control:
        - { path: ^/esi, roles: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 }
        - { path: ^/esi, roles: ROLE_NO_ACCESS }
<access-control>
    <rule path="^/esi" role="IS_AUTHENTICATED_ANONYMOUSLY" ip="127.0.0.1" />
    <rule path="^/esi" role="ROLE_NO_ACCESS" />
</access-control>
'access_control' => array(
    array('path' => '^/esi', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'ip' => '127.0.0.1'),
    array('path' => '^/esi', 'role' => 'ROLE_NO_ACCESS'),
),

Ahora, cuando alguien trate de acceder por ejemplo a /esi/algo desde la IP 10.0.0.1:

  • La primera regla del control de acceso se ignora porque path concuerda pero la ip no.
  • La segunda regla del control de acceso se activa porque la URL de la petición coincide con la expresión regular de su opción path. El usuario verá denegado su acceso porque la ruta requiere que tenga el rol ROLE_NO_ACCESS, pero este rol no existe en la aplicación (en eso consiste este truco, en exigir un rol que no existe).

Ahora, si la misma petición proviene de la IP 127.0.0.1:

  • Se activa la primera regla del control de acceso porque tanto path como ip concuerdan: se permite el acceso al usuario porque todos los usuarios disponen del rol IS_AUTHENTICATED_ANONYMOUSLY (Symfony lo aplica automáticamente a todos los usuarios).
  • La segunda regla de acceso ni siquiera se tiene en cuenta porque la primera regla ha producido una coincidencia.

13.4.4. Protegiendo por canal

También puedes requerir que un usuario acceda a una URL vía SSL. Para ello, añade la opción requires_channel en cualquier entrada access_control:

# app/config/security.yml
security:
    # ...
    access_control:
        - { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
<access-control>
    <rule path="^/cart/checkout" role="IS_AUTHENTICATED_ANONYMOUSLY" requires_channel="https" />
</access-control>
'access_control' => array(
    array('path' => '^/cart/checkout', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'requires_channel' => 'https'),
),

13.4.5. Protegiendo un controlador

Proteger la aplicación en función de los patrones de las URL es fácil, pero puede no ser lo suficientemente granular. Si lo necesitas, también puedes restringir el acceso a un único controlador:

// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

public function helloAction($name)
{
    if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) {
        throw new AccessDeniedException();
    }

    // ...
}

También puedes optar por instalar y utilizar el bundle JMSSecurityExtraBundle, con el que puedes asegurar tu controlador usando anotaciones:

// ...
use JMS\SecurityExtraBundle\Annotation\Secure;

/**
 * @Secure(roles="ROLE_ADMIN")
 */
public function helloAction($name)
{
    // ...
}

Para más información, consulta la documentación de JMSSecurityExtraBundle. Si estás usando la distribución estándar de Symfony, este bundle ya está instalado y configurado.

13.4.6. Protegiendo otros servicios

En Symfony puedes proteger cualquier otro elemento utilizando una estrategia similar a la explicada en la sección anterior. Supongamos por ejemplo que tienes un servicio (es decir, una clase PHP), cuyo trabajo consiste en enviar mensajes de correo electrónico. Si lo deseas puedes restringir el uso de esta clase a aquellos usuarios que tengan un rol específico.

Para más información sobre cómo utilizar el componente de seguridad para proteger diferentes servicios y métodos en tu aplicación, consulta el artículo How to secure any Service or Method in your Application.

13.4.7. Listas de control de acceso (ACL)

Imagina que has desarrollado un sistema de blog donde los usuarios pueden comentar tus artículos. Además, quieres que los usuarios puedan modificar sus propios comentarios pero no los de otros usuarios. Por último, quieres que el usuario admin pueda modificar cualquier comentario.

El componente de seguridad incluye un sistema de listas de control de acceso (ACL) que puedes utilizar cuando necesites controlar el acceso a las instancias de un objeto. Sin ACL puedes proteger tu sistema para que sólo determinados usuarios puedan editar los comentarios del blog en general. Pero con ACL puedes restringir o permitir el acceso comentario por comentario.

Para más información, consulta el artículo How to use Access Control Lists (ACLs).