If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeAhora 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.
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_ADMIN
es 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!
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 tienesROLE_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_RESOURCES
configurado 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.
Hey Mepcuk!
Hmm. I'm not sure if this is really a "role hierarchy" that you need in the database, or just a smart voter system + smart queries.
The most important thing to focus on first is how you want to structure the data and relations in the database. It sounds to me like you have this setup:
A) worker 1 and worker 2 generally have similar access (i.e. they probably have similar or identical roles)
B) BUT, worker 1 can see "company 1" summary report only and worker 2 can see "company 2" summary report only.
If this is the case, then you need to protect the "summary" report with a voter - e.g. $this->denyAccessUnlessGranted('SUMMARY_REPORT', $company)
.
In the database, you will naturally have some way that "worker 1" and "company 1" are related. This part has nothing do with security, it's just part of your data model. For example, maybe your Worker entity has a ManyToOne to Company, which is how you know that worker 1 works for company 1.
Assuming you have the database all modeled how you want, then in your custom voter, you just use that relationship. For example, you would (in your voter) get the current user object (pretend it is "user 1") and then look at the $company object that was passed as the subject (pretend it is "company 1"). Then, if $user->getCompany() === $company, you know that access is granted. Else, access is denied.
Let me know if this helps!
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"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
}
}
Maybe off topic question - how to create hierarchy on Database level too.
I have worker 1 - have similar access that worker 2, but information what worker 1 see about summary report from company 1 and worker 2 summary from company 2. Twig for summary identical. I think i need to create method on DataRepository with argument ? It is correct way?