Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Jerarquía de roles

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Ahora mismo, nuestro sitio tiene dos tipos de usuarios: usuarios normales y usuarios administradores. Si eres un usuario normal, puedes votar en las respuestas y probablemente hacer un montón de cosas más una vez que hayamos terminado. Si eres un administrador, también puedes ir a la sección de administración.

Aquí todavía no hay mucho... pero en teoría, un usuario administrador podría tener acceso a editar preguntas, respuestas o gestionar los datos de los usuarios. Y... muchos sitios son así de sencillos: eres un usuario normal o un usuario administrador.

Organizar los nombres de los roles

Pero en una empresa más grande, las cosas pueden no ser tan sencillas: puedes tener muchos tipos de usuarios administradores. Algunos tendrán acceso a algunas secciones y otros a otras. La pregunta es: ¿cuál es la mejor manera de organizar nuestros roles para lograr esto?

Bueno, en realidad sólo hay dos posibilidades. La primera es asignar roles a los usuarios que se denominan según el tipo de usuario. Por ejemplo, asignas roles a usuarios como ROLE_HUMAN_RESOURCES o ROLE_IT o ROLE_PERSON_WHO_OWNS_THE_COMPANY. Luego, deniegas el acceso a los controladores utilizando estas cadenas. Pero... Esto no me gusta. Acabas en situaciones extrañas en las que, en un controlador, te das cuenta de que tienes que permitir el acceso a ROLE_HUMAN_RESOURCES o ROLE_IT, lo cual es un lío.

Bien, ¿cuál es la segunda opción? Proteger los controladores con nombres de rol que describan el acceso que te da ese rol. Por ejemplo, en la parte inferior de este controlador, vamos a crear una supuesta página de administrador para moderar las respuestas. Establece la URL como /admin/answers... y llámala adminAnswers():

... lines 1 - 10
class AdminController extends AbstractController
{
... lines 13 - 65
/**
* @Route("/admin/comments")
*/
public function adminComments()
{
... lines 71 - 72
return new Response('Pretend comments admin page');
}
}

Imagina que nuestro departamento de "recursos humanos" y el de informática deben tener acceso a esto. Pues bien, como he dicho antes, no quiero intentar poner aquí una lógica que permita ROLE_HUMAN_RESOURCES o ROLE_IT.

En su lugar, di $this->denyAccessUnlessGranted() y pasa esto ROLE_COMMENT_ADMIN, un nombre de rol que acabo de inventar que describe lo que se está protegiendo:

... lines 1 - 10
class AdminController extends AbstractController
{
... lines 13 - 65
/**
* @Route("/admin/comments")
*/
public function adminComments()
{
$this->denyAccessUnlessGranted('ROLE_COMMENT_ADMIN');
return new Response('Pretend comments admin page');
}
}

¡Oh, tonto Ryan! Debería haber llamado a esto ROLE_ANSWER_ADMIN - sigo usando "comentario" cuando quiero decir "respuesta". Esto funcionará bien - pero ROLE_ANSWER_ADMINes realmente el mejor nombre.

De todos modos, lo que me encanta de esto es lo claro que es el controlador: no puedes acceder a esto a menos que tengas un rol específico para este controlador. Sólo hay un problema: si vamos a /admin/answers, se nos deniega el acceso... porque no tenemos ese rol.

Probablemente puedes ver el problema de este enfoque. Cada vez que creemos una nueva sección y la protejamos con un nuevo nombre de rol, tendremos que añadir ese rol a cada usuario de la base de datos que deba tener acceso. ¡Eso parece un dolor de cabeza!

Hola role_hierarchy

Afortunadamente, Symfony tiene una función justo para esto, llamada jerarquía de roles. Abreconfig/packages/security.yaml y, en cualquier lugar dentro de aquí... pero lo pondré cerca de la parte superior, añade role_hierarchy. Debajo de esto, di ROLE_ADMIN y pon esto en una matriz. Por ahora, sólo incluye ROLE_COMMENT_ADMIN:

security:
... lines 2 - 6
role_hierarchy:
ROLE_ADMIN: ['ROLE_COMMENT_ADMIN']
... lines 9 - 58

Esto parece tan simple como lo es. Dice:

Si tienes ROLE_ADMIN, entonces automáticamente también tienes ROLE_COMMENT_ADMIN.

¿El resultado? Si refrescamos la página, ¡acceso concedido!

La idea es que, para cada "tipo" de usuario -como el de "recursos humanos", o el de informática-, crees un nuevo elemento en role_hierarchy para ellos, como ROLE_HUMAN_RESOURCESconfigurado con una matriz de los roles que deba tener.

Por ejemplo, supongamos que también protegemos otro controlador de administración con ROLE_USER_ADMIN:

security:
... lines 2 - 6
role_hierarchy:
... line 8
ROLE_HUMAN_RESOURCES: ['ROLE_USER_ADMIN']
... lines 10 - 59

En este caso, si tienes ROLE_HUMAN_RESOURCES, entonces obtienes automáticamenteROLE_USER_ADMIN... que te da acceso a modificar los datos del usuario. Y si tienesROLE_ADMIN, quizás también puedas acceder a esta sección:

security:
... lines 2 - 6
role_hierarchy:
ROLE_ADMIN: ['ROLE_COMMENT_ADMIN', 'ROLE_USER_ADMIN']
ROLE_HUMAN_RESOURCES: ['ROLE_USER_ADMIN']
... lines 10 - 59

Con esta configuración, cada vez que añadamos una nueva sección a nuestro sitio y la protejamos con un nuevo rol, sólo tendremos que ir a role_hierarchy y añadirla a los grupos que la necesiten. No necesitamos cambiar los roles en la base de datos para nadie. Y en la base de datos, la mayoría -o todos- los usuarios sólo necesitarán un rol: el que representa el "tipo" de usuario que son, como ROLE_HUMAN_RESOURCES.

Hablando de usuarios administradores, cuando estamos depurando un problema de un cliente en nuestro sitio, a veces sería muy útil que pudiéramos entrar temporalmente en la cuenta de ese usuario... sólo para ver lo que está viendo. En Symfony, eso es totalmente posible. Vamos a hablar de la suplantación de identidad a continuación.

Leave a comment!

¡Este tutorial también funciona muy bien para Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.6.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}