If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeMe 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 | |
} |
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.
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!');
}
// ...
}
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.
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!
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").
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!
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');
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!
// 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
}
}
Hey, I have PHP 8.2.0 and this line doesn't work
but with
@IsGranted("ROLE_ADMIN")
works as expected and throw:Access Denied by controller annotation @IsGranted("ROLE_ADMIN")