Buy Access to Course
13.

Denegar el acceso con la opción "seguridad

|

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

Acabamos de hablar mucho sobre la autenticación: es la forma de decirle a la API quiénes somos. Ahora pasamos a la autorización, que consiste en denegar el acceso a determinadas operaciones y otras cosas en función de quién eres.

Utilizar access_control

Hay múltiples formas de controlar el acceso a algo. La más sencilla es enconfig/packages/security.yaml. Igual que en la seguridad normal de Symfony, aquí abajo tenemos una sección access_control:

56 lines | config/packages/security.yaml
security:
// ... lines 2 - 37
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
// ... lines 43 - 56

Si quieres bloquear un patrón de URL específico por un rol específico, utilizaaccess_control. Podrías usar esto, por ejemplo, para requerir que el usuario tenga un rol para usar cualquier cosa en tu API apuntando a URLs que empiecen por /api.

Hola Opción "seguridad

En una aplicación web tradicional, utilizo access_control para varias cosas. Pero la mayoría de las veces pongo mis reglas de autorización dentro de los controladores. Pero... por supuesto, con API Platform, no tenemos controladores. Todo lo que tenemos son clases de recursos API, como DragonTreasure. Así que en lugar de poner las reglas de seguridad en los controladores, las adjuntaremos a nuestras operaciones.

Por ejemplo, hagamos que la petición POST para crear un nuevo DragonTreasure requiera que el usuario esté autenticado. Para ello, añadiremos una opción muy útil security. Establécela como una cadena y dentro de, digamos is_granted(), comillas dobles y luegoROLE_TREASURE_CREATE:

237 lines | src/Entity/DragonTreasure.php
// ... lines 1 - 26
#[ApiResource(
// ... lines 28 - 29
operations: [
// ... lines 31 - 36
new Post(
security: 'is_granted("ROLE_TREASURE_CREATE")',
),
// ... lines 40 - 41
],
// ... lines 43 - 56
)]
// ... lines 58 - 75
class DragonTreasure
{
// ... lines 78 - 235
}

Podríamos utilizar simplemente ROLE_USER si sólo quisiéramos asegurarnos de que el usuario ha iniciado sesión. Pero tenemos un sistema genial en el que, si utilizas un token de API para la autenticación, ese token tendrá ámbitos específicos. Uno de los posibles ámbitos se llamaSCOPE_TREASURE_CREATE... que se asigna a ROLE_TREASURE_CREATE. Así que lo buscamos. Además, en security.yaml, a través de role_hierarchy, si inicias sesión a través del formulario de inicio de sesión, obtienes ROLE_FULL_USER... y entonces automáticamente también obtienesROLE_TREASURE_CREATE.

En otras palabras, al utilizar ROLE_TREASURE_CREATE, se te concederá el acceso porque te has conectado a través del formulario de inicio de sesión o te has autenticado utilizando un token de API que tiene ese alcance.

Vamos a probarlo. Asegúrate de que has cerrado la sesión. Voy a actualizar. Sí, puedes ver en la barra de herramientas de depuración web que no he iniciado sesión... y Swagger no tiene actualmente un token de API.

Vamos a probar la ruta POST. Pruébalo... y... ejecuta con los datos del ejemplo. Y... ¡sí! ¡Un código de estado 401 con el tipo hydra:error!

Más información sobre el atributo "seguridad

La opción security contiene en realidad una expresión que utiliza el lenguaje de expresión de Symfony. Y puedes ponerte muy elegante con ella. Aunque, vamos a intentar mantener las cosas simples. Y más adelante, aprenderemos cómo descargar reglas complejas a los votantes.

Añadamos algunas reglas más. Put y Patch son ediciones. Son especialmente interesantes porque, para utilizarlas, no sólo necesitamos estar conectados, sino que probablemente necesitemos ser el propietario de este DragonTreasure. No queremos que otras personas editen nuestras cosas.

Nos preocuparemos de la parte de la propiedad más adelante. Pero por ahora, al menos añadamos security con is_granted() y luego ROLE_TREASURE_EDIT:

245 lines | src/Entity/DragonTreasure.php
// ... lines 1 - 27
#[ApiResource(
// ... lines 29 - 30
operations: [
// ... lines 32 - 40
new Put(
security: 'is_granted("ROLE_TREASURE_EDIT")',
),
// ... lines 44 - 49
],
// ... lines 51 - 64
)]
// ... lines 66 - 83
class DragonTreasure
{
// ... lines 86 - 243
}

Una vez más, estoy utilizando el rol scope. Cópialo y duplícalo aquí abajo para Patch:

245 lines | src/Entity/DragonTreasure.php
// ... lines 1 - 27
#[ApiResource(
// ... lines 29 - 30
operations: [
// ... lines 32 - 43
new Patch(
security: 'is_granted("ROLE_TREASURE_EDIT")',
),
// ... lines 47 - 49
],
// ... lines 51 - 64
)]
// ... lines 66 - 83
class DragonTreasure
{
// ... lines 86 - 243
}

Ah, y antes hemos eliminado la operación Delete. Añadámosla de nuevo consecurity configurada para buscar ROLE_ADMIN:

245 lines | src/Entity/DragonTreasure.php
// ... lines 1 - 27
#[ApiResource(
// ... lines 29 - 30
operations: [
// ... lines 32 - 46
new Delete(
security: 'is_granted("ROLE_ADMIN")',
),
],
// ... lines 51 - 64
)]
// ... lines 66 - 83
class DragonTreasure
{
// ... lines 86 - 243
}

Si más adelante decidiéramos añadir un ámbito que permitiera a los tokens de la API eliminar tesoros, podríamos añadirlo y cambiar esto a ROLE_TRESURE_DELETE.

¡Asegurémonos de que esto funciona! Utiliza la ruta de recolección GET. Pruébalo. Esta operación no requiere autenticación... así que funciona bien. Y tenemos un tesoro con ID 1. Cierra esto, abre la operación PUT, pulsa "Probar", 1, "Ejecutar" y... ¡bien! ¡Aquí también obtenemos un 401!

Añadir "seguridad" a toda una clase

Así que añadir la opción security a las operaciones individuales es probablemente lo más habitual. Pero también puedes añadirla al propio ApiResource para que se aplique a toda la clase. Por ejemplo, en User, probablemente queramos que todas las operaciones requieran autenticación... excepto Post para crear, porque así es como se registraría un nuevo usuario.

Así que aquí arriba, añade security y busca ROLE_USER... sólo para comprobar que estamos registrados:

252 lines | src/Entity/User.php
// ... lines 1 - 20
#[ApiResource(
// ... lines 22 - 23
security: 'is_granted("ROLE_USER")',
)]
// ... lines 26 - 40
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
// ... lines 43 - 250
}

Y como esta clase tiene un recurso secundario... y esto también nos permite obtener un usuario, asegúrate de añadir aquí también security:

252 lines | src/Entity/User.php
// ... lines 1 - 25
#[ApiResource(
// ... lines 27 - 35
security: 'is_granted("ROLE_USER")',
)]
// ... lines 38 - 40
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
// ... lines 43 - 250
}

Vigila la seguridad si utilizas subrecursos.

Vale, ahora todas las operaciones en User requieren que estés conectado. Pero... no queremos eso para la operación Post. Para añadir flexibilidad, sube a la primeraApiResource, añade la opción operations y, muy rápido, enumera todas las operaciones normales, new Get(), new GetCollection(), new Post(), new Put(),new Patch() y new Delete():

272 lines | src/Entity/User.php
// ... lines 1 - 25
#[ApiResource(
// Now add `operations` set to the 6 normal operations
operations: [
new Get(),
new GetCollection(),
new Post(
// ... line 32
),
new Put(
// ... line 35
),
new Patch(
// ... line 38
),
new Delete(),
],
// ... lines 42 - 44
)]
// ... lines 46 - 60
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
// ... lines 63 - 270
}

Ahora que las tenemos, podemos personalizarlas. Para Post, como queremos que no requiera autenticación, digamos que security: 'is_granted() pasa un rol especial falso llamado PUBLIC_ACCESS:

272 lines | src/Entity/User.php
// ... lines 1 - 25
#[ApiResource(
// Now add `operations` set to the 6 normal operations
operations: [
// ... lines 29 - 30
new Post(
security: 'is_granted("PUBLIC_ACCESS")',
),
// ... lines 34 - 40
],
// ... lines 42 - 44
)]
// ... lines 46 - 60
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
// ... lines 63 - 270
}

Esto anulará la regla de seguridad que estamos pasando a nivel de recurso. Ah, y ya que estamos aquí, para Put, configura security para que busque ROLE_USER_EDIT ya que tenemos un rol de ámbito para editar usuarios. Repite eso aquí abajo para Patch:

272 lines | src/Entity/User.php
// ... lines 1 - 25
#[ApiResource(
// Now add `operations` set to the 6 normal operations
operations: [
// ... lines 29 - 33
new Put(
security: 'is_granted("ROLE_USER_EDIT")'
),
new Patch(
security: 'is_granted("ROLE_USER_EDIT")'
),
// ... line 40
],
// ... lines 42 - 44
)]
// ... lines 46 - 60
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
// ... lines 63 - 270
}

¡Me encanta! Actualiza toda la página. Lo que más nos interesa es la ruta POST usuarios. No estamos autentificados, así que pulsa "Probar" y dejaré los datos por defecto. "Ejecutar" y... ¡lo hemos clavado! Un estado 201. Eso sí permitía el acceso anónimo.

Comprobación de las decisiones de seguridad

Ah, y superdivertido: si alguna vez quieres ver las decisiones de seguridad que se tomaron durante una petición, abre el perfilador de esa petición, baja a la sección "Seguridad" y luego a "Decisión de acceso". Para esta petición, el sistema de seguridad sólo tomó una decisión: era para PUBLIC_ACCESS, y estaba permitida.

Siguiente: nuestra API se está volviendo compleja... y sólo va a volverse más compleja. Es hora de dejar de probar nuestras rutas manualmente mediante Swagger y empezar a probarlas con pruebas automatizadas.