Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Negar el acceso en un controlador

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

Me gusta utilizar el control de acceso en security.yaml para ayudarme a proteger secciones enteras de mi sitio... como si todo lo que está bajo /admin requiriera algún rol:

security:
... lines 2 - 50
access_control:
- { path: ^/admin, roles: ROLE_USER }
... lines 53 - 54

Pero la mayoría de las veces, protejo mi sitio en base a un controlador por otro.

Abre QuestionController y encuentra la acción new():

... lines 1 - 17
class QuestionController extends AbstractController
{
... lines 20 - 45
/**
* @Route("/questions/new")
*/
public function new()
{
return new Response('Sounds like a GREAT feature for V2!');
}
... lines 53 - 86
}

Esto... obviamente... no es una página real... pero vamos a terminarla algún día... probablemente.

Imaginemos que esta página sí funciona y que cualquiera en nuestro sitio debería poder hacer nuevas preguntas... pero es necesario estar conectado para cargar esta página. Para imponerlo, en el controlador -en la primera línea- pongamos$this->denyAccessUnlessGranted('ROLE_USER'):

... lines 1 - 17
class QuestionController extends AbstractController
{
... lines 20 - 45
/**
* @Route("/questions/new")
*/
public function new()
{
$this->denyAccessUnlessGranted('ROLE_USER');
... lines 52 - 53
}
... lines 55 - 88
}

Así que si el usuario no tiene ROLE_USER - lo que sólo es posible si no está conectado - entonces niega el acceso. Sí, denegar el acceso en un controlador es así de fácil.

Cerremos la sesión... y vayamos a esa página: /questions/new. ¡Qué bonito! Como somos anónimos, nos redirige a /login. Ahora iniciemos sesión - abraca_admin@example.com, contraseña tada y... ¡acceso concedido!

Si cambiamos a ROLE_ADMIN... que no es un rol que tengamos, obtenemos acceso denegado:

... lines 1 - 17
class QuestionController extends AbstractController
{
... lines 20 - 45
/**
* @Route("/questions/new")
*/
public function new()
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
... lines 52 - 53
}
... lines 55 - 88
}

La excepción AccessDeniedException

Una cosa genial del método denyAccessUnlessGranted() es que no devolvemos el valor. Podemos decir simplemente $this->denyAccessUnlessGranted() y eso interrumpe el controlador...., lo que significa que el código de aquí abajo nunca se ejecuta.

Esto funciona porque, para denegar el acceso en Symfony, en realidad se lanza una clase de excepción especial: AccessDeniedException. Esta línea lanza esa excepción.

En realidad, podemos reescribir este código de forma más larga... sólo para aprender. Esta única línea es idéntica a decir: si no $this->isGranted('ROLE_ADMIN') -isGranted() es otro método de ayuda en la clase base - entonces lanza esa excepción especial diciendo throw $this->createAccessDeniedException() con:

¡No hay acceso para ti!

... lines 1 - 17
class QuestionController extends AbstractController
{
... lines 20 - 45
/**
* @Route("/questions/new")
*/
public function new()
{
if (!$this->isGranted('ROLE_ADMIN')) {
throw $this->createAccessDeniedException('No access for you!');
}
... lines 54 - 55
}
... lines 57 - 90
}

Eso hace lo mismo que antes.... y el mensaje que pases a la excepción sólo lo verán los desarrolladores. Mantén pulsado Command o Ctrl para saltar al método createAccessDeniedException()... puedes ver que vive enAbstractController. Este método es tan bonito y aburrido: crea y devuelve un nuevo AccessDeniedException. Esta excepción es la clave para denegar el acceso, y podrías lanzarla desde cualquier parte de tu código.

Cierra eso... y luego ve a actualizar. Sí, obtenemos lo mismo que antes.

Negar el acceso con la anotación/atributo IsGranted

Hay otra forma interesante de denegar el acceso en un controlador... y funciona si tienes instalado sensio/framework-extra-bundle, como es nuestro caso. En lugar de escribir tus reglas de seguridad en PHP, puedes escribirlas como anotaciones o atributos de PHP. Compruébalo: encima del controlador, di @IsGranted() - le daré al tabulador para autocompletarlo y así obtener la declaración use - y luego "ROLE_ADMIN":

... lines 1 - 12
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
... lines 14 - 18
class QuestionController extends AbstractController
{
... lines 21 - 46
/**
... line 48
* @IsGranted("ROLE_ADMIN")
*/
public function new()
{
return new Response('Sounds like a GREAT feature for V2!');
}
... lines 55 - 88
}

Si intentamos esto... ¡acceso denegado! Nosotros, como desarrolladores, vemos un mensaje de error ligeramente diferente, pero el usuario final vería la misma página de error 403. Ah, y si utilizas PHP 8, puedes utilizar IsGranted como atributo PHP en lugar de una anotación:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;

class QuestionController extends AbstractController
{
    // ...
    /**
     * ...
     * #[IsGranted("ROLE_ADMIN")]
     */
    public function new()
    {
        return new Response('Sounds like a GREAT feature for V2!');
    }
    // ...
}

Negar el acceso a toda una clase de controlador

Una de las cosas más interesantes de la anotación o atributo IsGranted es que puedes utilizarlo sobre la clase del controlador. Así que por encima de QuestionController, añade @IsGranted("ROLE_ADMIN"):

use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;

/**
 * @IsGranted("ROLE_ADMIN")
 */
class QuestionController extends AbstractController
{
    // ...
    public function new()
    {
        return new Response('Sounds like a GREAT feature for V2!');
    }
    // ...
}

De repente, ROLE_ADMIN será necesario para ejecutar cualquier controlador de este archivo. Yo no haré esto... porque entonces sólo los usuarios administradores podrían acceder a mi página web, pero es una gran característica.

Bien, volvamos a new(), cambiemos esto por ROLE_USER... para que la página vuelva a funcionar:

... lines 1 - 18
class QuestionController extends AbstractController
{
... lines 21 - 46
/**
... line 48
* @IsGranted("ROLE_USER")
*/
public function new()
{
... line 53
}
... lines 55 - 88
}

Ahora mismo, todos los usuarios sólo tienen ROLE_USER. Así que lo siguiente: vamos a empezar a añadir roles adicionales a algunos usuarios en la base de datos para diferenciar entre los usuarios normales y los administradores. También aprenderemos a comprobar las reglas de autorización en Twig para poder mostrar condicionalmente los enlaces -como "iniciar sesión" o "cerrar sesión"- en la situación adecuada.

Leave a comment!

7
Login or Register to join the conversation
Szymon Avatar

Hey, I have PHP 8.2.0 and this line doesn't work

#[IsGranted("ROLE_ADMIN")]

but with @IsGranted("ROLE_ADMIN") works as expected and throw:
Access Denied by controller annotation @IsGranted("ROLE_ADMIN")

Reply

Hey Szymon,

I believe you're not using PHP attributes to load your routes. In your config/routes.yaml file check if the type key is set to attribute

Cheers!

Reply
Szymon Avatar

oh yea, I uncommented this lines:

controllers:
#    resource:
#        path: ../src/Controller/
#        namespace: App\Controller
    type: attribute

but then following problem has occur:

The "type" key for the route definition "controllers" in "C:\...\cauldron_overflow/config/routes.yaml" is unsupported. It is only available for imports in combination with the "resource" key in C:\...\cauldron_overflow/config/routes.yaml (which is being imported from "C:\...\cauldron_overflow\src\Kernel.php").
Reply

Hey @Szymon!

Ok, let's see here :). First, I would undo the change you made to config/routes.yaml. I don't think that is the problem here. If you are able to create #[Route()] attributes in your controller and those routes are working, then this import is correct!

So, back to your original issue where #[IsGranted("ROLE_ADMIN")] is NOT working but the "annotation" version IS working. This was maybe our fault: I just realized that the code example we were showing on this page was incorrect: we had the attribute INSIDE of a PHP comment. I just fixed it, it should look like this:

    /**
     * ...
     */
    #[IsGranted("ROLE_ADMIN")]
    public function new()
    {
        return new Response('Sounds like a GREAT feature for V2!');
    }

Does that help? Or were you already doing this, and it didn't help?

Cheers!

1 Reply
Szymon Avatar

yep, it's working properly :) thanks for explanation!!!

Reply
Kevin Avatar
Kevin Avatar Kevin | posted hace 1 año | edited

If sensio/framework-extra-bundle wasn't installed would adding this line to every method be the correct way of denying access to an entire controller? Was curious if there was an easier way than that.

$this->denyAccessUnlessGranted('ROLE_USER');

Reply

Hey Kevin!

That's a good question. I don't believe that there is any way "out-of-the-box" to deny access to an entire controller class without SensioFrameworkeExtraBundle. So yes, you would need to add that above each method. You could, of course, mess around with event listeners that read your controller class before it's called and run some security check... but then all your logic is hidden away in some class far away from your routes or controllers... which I don't love :/.

Cheers!

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": ">=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
    }
}
userVoice