Introducción al control de acceso basado en roles de Sylius

16 de junio de 2015

Como siempre el proyecto Sylius nos sorprende con magníficas funcionalidades que podemos usar en nuestras aplicaciones. En este caso hablaremos de una de las últimas funcionalidades agregada en la versión 0.14 de Sylius, el sistema jerárquico de control de acceso basado en roles (Hierarchical Role-Based-Access-Control o RBAC).

Este sistema permite definir árboles de permisos y roles para ser asignados a usuarios específicos. De esta forma se puede restringir el acceso a secciones específicas del sitio web o la administración. Es muy fácil introducir nuestros propios permisos y el sistema es bastante flexible. Y como todo en Sylius, está disponible para usar en cualquier aplicación Symfony como un bundle independiente.

¿Por qué RBAC?

  • Las aplicaciones grandes tienen muchos Permisos que gestionar.
  • Los Permisos están conectados con los Roles, y no con los Usuarios directamente.
  • Los Roles de los Usuarios en un sistema pueden cambiar, sus Permisos deben cambiar también de forma inmediata.
  • La gestión de Permisos por Usuarios puede ser trabajosa y requerir de muchos recursos.

¿Qué es Rbac?

El componente Sylius Rbac implementa una variante jerárquica de RBAC, que significa que separan los conceptos de Usuarios (Identidad), Roles y Permisos.

Cada Identidad puede tener multiples Roles, que heredan todos los permisos de sus roles hijos. Los permisos son definidos como un árbol también, los niveles superiores poseen permisos definidos en los nodos inferiores.

Entonces tendremos que:

  • Un Usuario posee muchos Roles, un Rol posee muchos Permisos, los Permisos poseen configuraciones que definen el acceso al sistema.
  • Un Rol puede tener muchos Roles hijos. El Rol padre posee tanto sus propios Permisos como todos los permisos de sus Roles hijos.
  • Un Permiso puede tener muchos Permisos hijos. El Permiso padre posee tanto sus propias configuraciones como todas las configuraciones de sus Permisos hijos.
  • Un Rol también posee un arreglo de permisos de seguridad estandar de Symfony (ROLE_<permiso>).

Instalación

Instalando el bundle y sus dependencias con Composer:

$ composer require sylius/rbac-bundle:0.14.0

Nota: La versión mínima para usar el bundle es 0.14. Al momento de escribir este artículo esta era la última versión. Puede actualizar el número de versión hasta su última actualización si así lo desea para futuras versiones.

Agregar los nuevos bundles y sus dependencias a AppKernel.php. Este bundle depende tanto de SyliusResourceBundle como deDoctrineCacheBundle yStofDoctrineExtensionBundle así como sus respectivas dependencias:

<?php
// app/AppKernel.php
public function registerBundles()
{
    $bundles = array(
        new FOS\RestBundle\FOSRestBundle(),
        new JMS\SerializerBundle\JMSSerializerBundle($this),
        new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
        new WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(),
        new Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle(),
 
        new Sylius\Bundle\RbacBundle\SyliusRbacBundle(),
        new Sylius\Bundle\ResourceBundle\SyliusResourceBundle(),
 
        // Other bundles...
        new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
    );
}

Importante: Por favor registre los bundles antes de DoctrineBundle. Esto es importante ya que hay listeners que necesitan ejecutarse primero.

Aunque por el momento el bundle solo soporta Doctrine ORM se debe configurar el driver a utilizar como todos los bundle de Sylius:

# app/config/config.yml
sylius_rbac:
    driver: doctrine/orm

Además asegurar que la configuración de tiempo (timestampable) y de árbol (tree) de la librería stof_doctrine_extensionsestén activadas:

# app/config/config.yml
stof_doctrine_extensions:
    orm:
        default:
            timestampable: true
            tree: true

Tu clase Usuario necesita implementar la interfazSylius\Component\Rbac\Model\IdentityInterface y definir la asociación de roles:

<?php
// src/App/AppBundle/Entity/User.php
namespace App\AppBundle\Entity;
 
use Doctrine\Common\Collections\ArrayCollection;
use Sylius\Component\Rbac\Model\IdentityInterface;
use Sylius\Component\Rbac\Model\RoleInterface;
 
/**
 * @ORM\Entity
 * @ORM\Table(name="app_user")
 */
class User implements IdentityInterface
{
    /**
     * @var ArrayCollection| RoleInterface[]
     *
     * @ORM\ManyToMany(targetEntity="Sylius\Component\Rbac\Model\RoleInterface")
     * @ORM\JoinTable(name="app_user_roles",
     *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false, onDelete="CASCADE")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id", nullable=false, onDelete="CASCADE")}
     *      )
     */
    private $authorizationRoles;
 
    public function __construct()
    {
        $this->authorizationRoles = new ArrayCollection();
    }
    /**
     * Método implementado para la interfaz IdentityInterface que devuelve los roles del usuarios
     *
     * @return ArrayCollection|\Sylius\Component\Rbac\Model\RoleInterface[]
     */
    public function getAuthorizationRoles()
    {
        return $this->authorizationRoles;
    }
 
    /*********** Funciones propias para agregar o quita roles ***********/        
    /**
     * @param RoleInterface $role
     * @return ArrayCollection|\Sylius\Component\Rbac\Model\RoleInterface[]
     */
    public function addAuthorizationRole(RoleInterface $role)
    {
        $this->authorizationRoles->add($role);
 
        return $this->authorizationRoles;
    }
 
    /**
     * @param RoleInterface $role
     * @return ArrayCollection|\Sylius\Component\Rbac\Model\RoleInterface[]
     */
    public function removeAuthorizationRole(RoleInterface $role)
    {
        $this->authorizationRoles->removeElement($role);
 
        return $this->authorizationRoles;
    }
 
    /**
     * @param ArrayCollection|\Sylius\Component\Rbac\Model\RoleInterface[] $authorizationRoles
     */
    public function setAuthorizationRoles($authorizationRoles)
    {
        $this->authorizationRoles = $authorizationRoles;
    }
}

Nota: Recuerde que trabaja con árboles de roles y permisos por lo que agregar o eliminar roles y permisos puede ser un poco más complicado que las propuesta de estos métodos que aquí pongo.

Y para finalizar actualizar el esquema de la base de datos:

 $ php app/console doctrine:schema:update --force

Definiendo permisos mediante configuración

Una de las facilidades de este bundle es cargar mediante configuración los roles y permisos de la aplicación para luego portarlos a la base de datos. El mecanísmo es muy sencillo y fácil de manejar.

Lo primero sería definir los permisos. Imaginemos que tenemos un simple CMS para agregar post a nuestra aplicación. El flujo de publicación de los post sería dirigido por editores y revisores. Los editores pueden crear, editar, ver y eliminar los post, mientras que los revisores pueden editar y ver los post solamente. Por otra parte también tendríamos administradores. Nuestra configuración para este sencillo ejemplo quedaría de la siguiente forma:

# app/config/config.yml
sylius_rbac:
    # ... otras configuraciones del bundle
 
    # Definición de cada uno de los permisos que posee en el sistema.
    permissions:
        # Permisos relacionados con la entidad Post
        app.manage.post: Gestor de Post
        app.post.show: Mostrar Post
        app.post.index: Listar Posts
        app.post.create: Crear Post
        app.post.update: Editar Post
        app.post.delete: Eliminar Post
 
        # Permisos relacionados con otras entidades
        # e.j.: app.manage.user: Gestor de usuarios
 
    # Definición de la jeraquía de permisos
    permissions_hierarchy:
        # El permiso app.manage.post va a poseer todos los permisos relacionado con los Post
        app.manage.post: [app.post.show, app.post.index, app.post.create, app.post.update, app.post.delete]
 
    # Definición de los roles del sistema
    roles:
        # El rol administrador no necesita ningún permiso especial para nuestra configuración específica
        administrator:
            name: Administrador
            description: Administrador del sistema
            security_roles: [ROLE_ADMIN]
        # El rol editor relaliza todas las funciones sobre los post por lo que solo necesita el permiso 'Gestor de usuario' que hereda los demás.
        editor:
            name: Editor
            description: Personas responsables de editar post
            permissions: [app.manage.post]
            security_roles: [ROLE_ADMINISTRATION_ACCESS]
        # El rol revisor solo posee los roles de mostrar y editar post
        reviewer:
            name: Revisor
            description: Personas responsables de aprobar los post de los editores
            permissions: [app.post.show, app.post.update]
            security_roles: [ROLE_ADMINISTRATION_ACCESS]
 
    # Definición de la jerarquia de roles
    roles_hierarchy:
        # El rol administrador pose los permisos de los roles Editor y Revisor
        administrator: [editor, reviewer]
 
    # Definición de los roles de seguridad
    security_roles:
        ROLE_ADMIN: Full action in administration
        ROLE_ADMINISTRATION_ACCESS: Can access to administration

Nota: La notación <aplicación>.<recurso>.<permiso_mínimo>, dondepermiso_minimo puede ser indexshow,updatecreate y delete definida para los permisos es usada como convención por SyliusResourceBundle para el control de acceso a la gestión de recursos del controlador ResourceController. Pero si no usa este bundle en sus proyectos no es necesario seguir esta convención y puede llamar a sus permisos como desee siempre que se expliquen por si solos.

Por último podemos cargar esta configuración en base de datos mediante el siguiente comando:

$ app/console sylius:rbac:initialize

Asignado permisos

Trabajar con los Roles, Permisos y Usuarios es bastante sencillo desde nuestros controladores o servicios:

<?php
namespace AppBundle\Controller;
 
use AppBundle\Entity\User;
use Sylius\Component\Rbac\Model\IdentityInterface;
use Sylius\Component\Rbac\Model\PermissionInterface;
use Sylius\Component\Rbac\Model\RoleInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 
class DefaultController extends Controller
{
    public function trabajandoConPermisosAction()
    {
        /******************* Creando permisos **********************/
        /** Obtenemos el permiso raíz del árbol de permisos */
        if (null === $rootPermission = $this->get('sylius.repository.permission')->findOneBy(
                array('code' => 'root')
            )
        ) {
            /** @var PermissionInterface $rootPermission */
            $rootPermission = $this->get('sylius.repository.permission')->createNew();
            $rootPermission->setCode('root');
            $rootPermission->setDescription('Root');
 
            $this->get('sylius.manager.permission')->persist($rootPermission);
            $this->get('sylius.manager.permission')->flush();
        }
 
        /** Creamos un permiso padre de otro permiso **/
        /** @var PermissionInterface $permissionParent */
        $permissionParent = $this->get('sylius.repository.permission')->createNew();
        $permissionParent->setCode('app.manage.user');
        $permissionParent->setDescription('Gestionar usuarios');
 
        /** Todos los permisos necesitan un permiso padre. El permiso creado tendrá de padre al permiso raíz */
        $permissionParent->setParent($rootPermission);
 
        /** Creamos un permiso hijo para el permiso creado */
        /** @var PermissionInterface $permissionChild */
        $permissionChild = $this->get('sylius.repository.permission')->createNew();
        $permissionChild->setCode('app.user.create');
        $permissionChild->setDescription('Crear nuevo usuario');
 
        /** El padre de este permiso es el permiso padre creado anteriormente */
        $permissionChild->setParent($permissionParent);
 
        $this->get('sylius.manager.permission')->persist($permissionParent);
        $this->get('sylius.manager.permission')->persist($permissionChild);
        $this->get('sylius.manager.permission')->flush();
 
        /******************* Creando roles ******************/
        /** Obtenemos el role raíz */
        if (null === $rootRole = $this->get('sylius.repository.role')->findOneBy(array('code' => 'root'))) {
            /** @var RoleInterface $rootRole */
            $rootRole = $this->get('sylius.repository.role')->createNew();
            $rootRole->setCode('root');
            $rootRole->setName('Root');
 
            /** El role raiz posee el permiso raiz como permiso principal */
            $rootRole->addPermission($rootPermission);
 
            $this->get('sylius.manager.role')->persist($rootRole);
            $this->get('sylius.manager.role')->flush();
        }
 
        /** Creamos un rol administrador que va a poseer el permiso padre creado anteriormente */
        /** @var RoleInterface $role */
        $role = $this->get('sylius.repository.role')->createNew();
        $role->setCode('administrator');
        $role->setName('Administrador');
        $role->setDescription('Administrador de usuarios');
        $role->addPermission($permissionParent);
 
        /** El padre de este permiso será el role raíz */
        $role->setParent($rootRole);
 
        $this->get('sylius.manager.role')->persist($role);
        $this->get('sylius.manager.role')->flush();
 
        /** El usuario debe implementar la interfaz IdentityInterface y los métodos necesarios para asignar o eliminar roles */
        /** @var IdentityInterface|User $user */
        $user = $this->get('doctrine.orm.entity_manager')->getRepository('AppBundle:User')->find(1);
        $user->addAuthorizationRole($role);
        $this->get('doctrine.orm.entity_manager')->persist($user);
        $this->get('doctrine.orm.entity_manager')->flush();
 
    }
}

Y listo su usuario ahora posee permisos en base a roles.

Chequeando permisos

El chequeo de permisos es muy sencillo y similar al usado por Symfony. Sylius nos brinda el serviciosylius.authorization_checker con el que mediante el método isGrantedpodemos chequear el acceso al usuario autenticado en el sistema.

Usando el SyliusResourceBundle:

El controlador de SyliusResourceBundle usa un convención de forma automática para chequear los permisos sin ningún esfuerzo. Para un recurso definido como app.user, donde app es el nombre de la aplicación y user el recurso, el bundle verificará si el usuario registrado posee el permiso en dependencia de la acción realizada. Para listar los usuario verifica el permisoapp.user.index, para mostrar los usuarios app.user.show, editarapp.user.update, crear app.user.create y eliminarapp.user.delete.

Desde cualquier controlador:

namespace App\Bundle\AppBundle\Controller;
 
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 
class YourController extends Controller
{
    public function securedAction(Request $request)
    {
        if (!$this->get('sylius.authorization_checker')->isGranted('your.permission.code')) {
           throw new AccessDeniedHttpException();
        }
    }
}

Desde una plantilla twig:

{% if sylius_is_granted('your.permission.code') %}
    {{ product.price %}
{% endif %}

Configuracion del bundle

sylius_rbac:
      driver: ~ # The driver used for persistence layer. Currently only `doctrine/orm` is supported.
      authorization_checker: sylius.authorization_checker.default
      identity_provider: sylius.authorization_identity_provider.security
      permission_map: sylius.permission_map.cached

      security_roles:
            ROLE_ADMINISTRATION_ACCESS: Can access backend

      roles:
            app.admin:
                name: Administrator
            app.cash_manager:
                name: Cash Manager
                description: People responsible for managing money.
                permissions: [app.view_cash, app.add_cash, app.remove_cash]
                security_roles: [ROLE_ADMINISTRATION_ACCESS]
      roles_hierarchy:
            app.admin: [app.cash_manager]

      permissions:
            app.view_cash: View cash
            app.add_cash: Add cash
            app.remove_cash: Remove cash
            app.manage_cash: Manage cash
      permissions_hierarchy:
            app.manage_cash: [app.view_cash, app.add_cash, app.remove_cash]

      classes:
          role:
              model:      Sylius\Component\Rbac\Model\Role # The role model class implementing `RoleInterface`.
              repository: ~ # Is set automatically if empty.
              controller: Sylius\Bundle\ResourceBundle\Controller\ResourceController
              form:
                    default: Sylius\Bundle\RbacBundle\Form\Type\RoleType
          permission:
              model:      Sylius\Component\Rbac\Model\Permission # The permission model class implementing `PermissionInterface`.
              repository: ~ # Is set automatically if empty.
              controller: Sylius\Bundle\ResourceBundle\Controller\ResourceController
              form:
                    default: Sylius\Bundle\RbacBundle\Form\Type\PermissionType
      validation_groups:
              role: [sylius]
              permission: [sylius]

Y esto es todo.

Recuerde que puede visitar la documentación oficial del proyecto.

Mejorar el rendimiento de Symfony Cmf Dynamic Routing utilizando la opción de configuración uriFilterRegexp

He estado trabajando por un tiempo con Symfony Cmf (SfCmf) y he desarrollado algunos sitios con él. Además he creado algunos bundles extras para imple...


Mejorando la experiencia de desarrollar aplicaciones Symfony con Flex

Después de algún tiempo de espera tras anunciar Flex, ya se encuentra disponible y público lo que será el nuevo juguetito de los desarrolladores Symfo...


IMPRESIONES DEL SEGUNDO ENCUENTRO DE DESARROLLADORES HABANA.

Hace solo algunos días que conozco este grupo de entusiastas y emprendedores que se comunican mediante Google Group bajo el nombre de Desarrolladores...