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!

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