Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Denegar el acceso con la opción "seguridad

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

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:

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:

... 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:

... 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:

... 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:

... 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:

... 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:

... 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():

... 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:

... 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:

... 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.

Leave a comment!

5
Login or Register to join the conversation

Hey SymfonyCasts friends,

There is a problem with this chapter's video. For some reason I get:

The media could not be loaded, either because the server or network failed or because the format is not supported.

But I was able to download the video from the download menu.

Cheers

Reply

A little more info, it might happen when I open back symfonycasts to catch up where I left off. I'm pretty sure I was able to play video 20 yesterday, but I opened back your site and now I have the same problem with that chapter. Chapter 21 plays just fine. And I still have a problem with this chapter (13).

Reply

Hey @julien_bonnier!

Hmm, sorry about that trouble :/. We had a few reports over the weekend like this - I'm wondering if Vimeo (our video host) had (or is having) some issues.

A little more info, it might happen when I open back symfonycasts to catch up where I left off

Do you mean that you, for example, have the browser open, close your computer for the day, come back tomorrow, and hit play again? It is possible there is some video problem if you're "resuming" in some way - so I'd love to know more.

Cheers!

Reply

Yep that's what I mean but it might also be a coincidence. ¯\_(⁠ツ⁠)_/⁠¯

Reply

Haha, right? Like many computer mysteries... coincidence? Or significant? lol

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.1.2
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.8.3
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.1
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.66.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.16.1
        "symfony/asset": "6.2.*", // v6.2.5
        "symfony/console": "6.2.*", // v6.2.5
        "symfony/dotenv": "6.2.*", // v6.2.5
        "symfony/expression-language": "6.2.*", // v6.2.5
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.5
        "symfony/property-access": "6.2.*", // v6.2.5
        "symfony/property-info": "6.2.*", // v6.2.5
        "symfony/runtime": "6.2.*", // v6.2.5
        "symfony/security-bundle": "6.2.*", // v6.2.6
        "symfony/serializer": "6.2.*", // v6.2.5
        "symfony/twig-bundle": "6.2.*", // v6.2.5
        "symfony/ux-react": "^2.6", // v2.7.1
        "symfony/ux-vue": "^2.7", // v2.7.1
        "symfony/validator": "6.2.*", // v6.2.5
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.3
        "symfony/browser-kit": "6.2.*", // v6.2.5
        "symfony/css-selector": "6.2.*", // v6.2.5
        "symfony/debug-bundle": "6.2.*", // v6.2.5
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.2.5
        "symfony/stopwatch": "6.2.*", // v6.2.5
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.5
        "zenstruck/browser": "^1.2", // v1.2.0
        "zenstruck/foundry": "^1.26" // v1.28.0
    }
}
userVoice