Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Denegación de acceso, access_control y 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

Ya hemos hablado mucho de la autenticación: el proceso de inicio de sesión. Y... incluso ya hemos iniciado la sesión. Así que vamos a echar nuestro primer vistazo a la autorización, que es la parte divertida en la que podemos ir de un lado a otro y denegar el acceso a diferentes partes de nuestro sitio.

Hola control_de_acceso

La forma más fácil de expulsar a alguien de tu fiesta es en realidad dentro deconfig/packages/security.yaml. Es a través de access_control:

security:
... lines 2 - 38
# 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 }

Descomenta la primera entrada:

security:
... lines 2 - 40
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }

El path es una expresión regular. Así que esto dice básicamente

Si una URL empieza por /admin -por tanto, /admin o /admin* -, entonces denegaré el acceso a menos que el usuario tenga ROLE_ADMIN.

Hablaremos más sobre los roles en un minuto... pero puedo decirte que nuestro usuario no tiene ese rol. Así que... vamos a intentar ir a una URL que coincida con esta ruta. En realidad tenemos una pequeña sección de administración en nuestro sitio. Asegúrate de que estás conectado... y luego ve a /admin. ¡Acceso denegado! Se nos expulsa con un error 403.

En producción, puedes personalizar el aspecto de esta página de error 403... además de personalizar la página de error 404 o 422.

¡Roles! Usuario::getRoles()

Hablemos de estos "roles". Abre la clase User:src/Entity/User.php. Así es como funciona. En el momento en que nos conectamos, Symfony llama a este método getRoles(), que forma parte de UserInterface:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 78
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
... lines 90 - 154
}

Devolvemos un array con los roles que debe tener este usuario. El comando make:usergeneró esto para que siempre tengamos un rol llamado ROLE_USER... más cualquier rol extra almacenado en la propiedad $this->roles. Esa propiedad contiene una matriz de cadenas... que se almacenan en la base de datos como JSON:

... lines 1 - 12
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 15 - 26
/**
* @ORM\Column(type="json")
*/
private $roles = [];
... lines 31 - 154
}

Esto significa que podemos dar a cada usuario tantos roles como queramos. Hasta ahora, cuando hemos creado nuestros usuarios, no les hemos dado ningún rol... por lo que nuestra propiedad roles está vacía. Pero gracias a cómo está escrito el método getRoles(), cada usuario tiene al menos ROLE_USER. El comando make:user generó el código así porque todos los usuarios necesitan tener al menos un rol... de lo contrario vagan por nuestro sitio como usuarios zombis medio muertos. No es... bonito.

Así que, por convención, siempre damos a un usuario al menos ROLE_USER. Ah, y la única regla sobre los roles -eso es un trabalenguas- es que deben empezar por ROLE_. Más adelante en el tutorial, aprenderemos por qué.

En cualquier caso, en el momento en que nos conectamos, Symfony llama a getRoles(), nos devuelve el array de roles, y los almacena. De hecho, podemos ver esto si hacemos clic en el icono de seguridad de la barra de herramientas de depuración de la web. ¡Sí! Roles: ROLE_USER.

Entonces, cuando vamos a /admin, esto coincide con nuestra primera entrada access_control, comprueba si tenemos ROLE_ADMIN, no lo tenemos, y deniega el acceso.

Sólo coincide UN control_de_acceso

Ah, pero hay un detalle importante que hay que saber sobre access_control: sólo se encontrará una coincidencia en una petición.

Por ejemplo, supón que tienes dos controles de acceso como éste:

security:
    # ...
    access_control:
      - { path: ^/admin, roles: ROLE_ADMIN }
      - { path: ^/admin/foo, roles: ROLE_USER }

Si fuéramos a /admin, eso coincidiría con la primera regla y sólo utilizaría la primera regla. Funciona como el enrutamiento: recorre la lista de control de acceso de uno en uno y, en cuanto encuentra la primera coincidencia, se detiene y utiliza sólo esa entrada.

Esto nos ayudará más adelante, cuando neguemos el acceso a toda una sección excepto a una URL. Pero por ahora, ¡sólo tenlo en cuenta!

Y... eso es todo. Los controles de acceso nos proporcionan una forma realmente sencilla de asegurar secciones enteras de nuestro sitio. Pero es sólo una forma de denegar el acceso. Pronto hablaremos de cómo podemos denegar el acceso controlador por controlador, algo que me gusta mucho.

Pero antes de hacerlo, sé que si intento acceder a esta página sin ROLE_ADMIN, obtengo el error 403 prohibido. ¿Pero qué pasa si intento acceder a esta página como usuario anónimo? Ve a /logout? Ahora no estamos conectados.

Vuelve a /admin y... ¡whoa! ¡Un error!

Se requiere una autentificación completa para acceder a este recurso.

A continuación, vamos a hablar del "punto de entrada" de tu cortafuegos: la forma en que ayudas a los usuarios anónimos a iniciar el proceso de acceso.

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