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!

9
Login or Register to join the conversation
Ruslan I. Avatar
Ruslan I. Avatar Ruslan I. | posted hace 9 meses

It's strange, when I go to /admin it says "no route found (404)" :(
No matter whether I uncommented the first line of access control in security.yaml or not.
And I'm logged in. When I logged out it's the same.

My Symfony version is 5.3 (all bundles have 5.3.* in composer.json, but by some reason web debug toolbar says 5.4.7)
php 7.4.28.
Windows

Result of symfony console debug:router


_preview_error ANY ANY ANY /_error/{code}.{_format}
_wdt ANY ANY ANY /_wdt/{token}
_profiler_home ANY ANY ANY /_profiler/
_profiler_search ANY ANY ANY /_profiler/search
_profiler_search_bar ANY ANY ANY /_profiler/search_bar
_profiler_phpinfo ANY ANY ANY /_profiler/phpinfo
_profiler_search_results ANY ANY ANY /_profiler/{token}/search/results
_profiler_open_file ANY ANY ANY /_profiler/open
_profiler ANY ANY ANY /_profiler/{token}
_profiler_router ANY ANY ANY /_profiler/{token}/router
_profiler_exception ANY ANY ANY /_profiler/{token}/exception
_profiler_exception_css ANY ANY ANY /_profiler/{token}/exception.css
answer_vote POST ANY ANY /answers/{id}/vote
app_popular_answers ANY ANY ANY /answers/popular
app_homepage ANY ANY ANY /{page}
app_question_new ANY ANY ANY /questions/new
app_question_show ANY ANY ANY /questions/{slug}
app_question_vote POST ANY ANY /questions/{slug}/vote
app_login ANY ANY ANY /login
app_logout ANY ANY ANY /logout
Reply
Ruslan I. Avatar

Hmmm, when I go to /admin I see in web debug toolbar that I'm logged out.
But when I go to the other pages on the site I'm still logged in.

Reply
Ruslan I. Avatar

Downgraded Symfony version to 5.3.16, but still route is not found.

Reply

Hey Ruslan I.!

Let's see if we can figure this out! I don't immediately see the problem, but I do notice a few things:

1) From your debug:router, it looks like, indeed, there is no route for /admin. So the problem isn't security or access_control... just that, somehow, the admin route isn't being seen! This page isn't something we built in this tutorial, but it should be included (it is, I just double-checked) in the "start/" directory of the code download. It's src/Controller/AdminController.php, and the Route annotation is above the dashboard() method. Do you have this file & method? What does it look like? I'm sure that the root of the issue is centered, somehow, around this routing being missing... or something weird happening.

2) You mentioned that when you go to /admin (the 404 page) you are logged out. But you ARE logged in on other pages. This is unrelated to your problem and is due to a "quirk" in Symfony. In Symfony, if you hit a 404 page, even if you ARE logged in on other pages, you will NOT be logged in on the error page. The reason is that the "listener" that actually "logs you in" at the start of every request runs *after* the routing system. So the routing system says "Ah! Route not found" and triggers the error page... before the authentication system ever has a chance to log you in. You can actually see this right here on SymfonyCasts: try going to some invented URL on SymfonyCasts and check the upper right: you'll see that you "appear" to not be logged in. It's an annoying quirk of Symfony.

Let me know what you discover!

Cheers!

1 Reply
Ruslan I. Avatar

Oh, I'm sorry, I'm idiot.

1) I thought that /admin page is some sort of basic Symfony's built in dashboard and tried to understand what's wrong with my bundles and configs.
But that's just my bad, because I'm not able to download course code due to payment issues that I already discussed with Victor Bocharsky. I use the same project building it step by step from first tutorial so I have no AdminController. I will try to continue this course as much as I can.
Thank you for quick reply, I was waiting for help. I appreciate that.

2) Hm. Thanks for this fact, it's really interesting. Now I know a little bit more.

Reply

Hey Ruslan I.!

> But that's just my bad, because I'm not able to download course code due to payment issues that I already discussed with Victor Bocharsky.

Ah, sorry about that! In that case, don't feel bad - I'm glad we could get this sorted out. The /admin page is just a basic, normal Symfony controller that renders a pretty boring page (just so we have a functional page at /admin).

> 2) Hm. Thanks for this fact, it's really interesting. Now I know a little bit more.

Awesome :)

Cheers!

1 Reply
discipolat Avatar
discipolat Avatar discipolat | posted hace 1 año

Hi, I'm facing a weird situation. When a logout from any page and then hit the back button (from the browser), i'm still able to see the last page until i refresh the page.

Reply

Hey discipolat!

Unfortunately... that's not so weird. It is, for better or worse, just how browsers work! Try it on some other site: log out, then click the back button. You'll go back to the previous page. This is because, when you hit back, there is no network request made to the site: your browser simply displays the previous page from cache.

If this is a problem, you could probably write some JavaScript that executes ever few seconds and that checks to see if the user is still authenticated. If they are not, you refresh the page via JavaScript.

Cheers!

Reply
discipolat Avatar

Thank's. I understand...basic indeed !!!

Reply
Cat in space

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

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