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 SubscribeI like using access control in security.yaml
to help me protect entire sections of my site... like everything under /admin
requires some role:
security: | |
... lines 2 - 50 | |
access_control: | |
- { path: ^/admin, roles: ROLE_USER } | |
... lines 53 - 54 |
But most of the time, I protect my site on a controller-by-controller basis.
Open QuestionController
and find the new()
action:
... 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 | |
} |
This... obviously... is not a real page... but we're totally going to finish it someday... probably.
Let's pretend that this page does work and anyone on our site should be allowed to ask new questions... but you do need to be logged in to load this page. To enforce that, in the controller - on the first line - let's $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 | |
} |
So if the user does not have ROLE_USER
- which is only possible if you're not logged in - then deny access. Yup, denying access in a controller is just that easy.
Let's log out... then go to that page: /questions/new
. Beautiful! Because we're anonymous, it redirected us to /login
. Now let's log in - abraca_admin@example.com
, password tada
and... access granted!
If we change this to ROLE_ADMIN
... which is not a role that we have, we get access denied:
... 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 | |
} |
One cool thing about the denyAccessUnlessGranted()
method is that we're not returning the value. We can just say $this->denyAccessUnlessGranted()
and that interrupts the controller.... meaning the code down here is never executed.
This works because, to deny access in Symfony, you actually throw a special exception class: AccessDeniedException
. This line throws that exception.
We can actually rewrite this code in a longer way... just for the sake of learning. This one line is identical to saying: if not $this->isGranted('ROLE_ADMIN')
- isGranted()
is another helper method on the base class - then throw that special exception by saying throw $this->createAccessDeniedException()
with:
No access for you!
... 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 | |
} |
That does the same thing as before.... and the message you pass to the exception is only going to be seen by developers. Hold Command
or Ctrl
to jump into the createAccessDeniedException()
method... you can see that it lives in AbstractController
. This method is so beautifully boring: it creates and returns a new AccessDeniedException
. This exception is the key to denying access, and you could throw it from anywhere in your code.
Close that... and then go refresh. Yup, we get the same thing as before.
There's one other interesting way to deny access in a controller... and it works if you have sensio/framework-extra-bundle
installed, which we do. Instead of writing your security rules in PHP, you can write them as PHP annotations or attributes. Check it out: above the controller, say @IsGranted()
- I'll hit tab to autocomplete that so I get the use
statement - then "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 | |
} |
If we try this... access denied! We as developers see a slightly different error message, but the end user would see the same 403 error page. Oh, and if you're using PHP 8, you can use IsGranted
as a PHP attribute instead of an annotation:
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!');
}
// ...
}
One of the coolest things about the IsGranted
annotation or attribute is that you can use it up on the controller class. So above QuestionController
, add @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!');
}
// ...
}
Suddenly, ROLE_ADMIN
will be required to execute any controller in this file. I won't do this... because then only admin users could access my homepage, but it's a great feature.
Ok, back down in new()
, let's change this to ROLE_USER
... so that the page kind of works again:
... lines 1 - 18 | |
class QuestionController extends AbstractController | |
{ | |
... lines 21 - 46 | |
/** | |
... line 48 | |
* @IsGranted("ROLE_USER") | |
*/ | |
public function new() | |
{ | |
... line 53 | |
} | |
... lines 55 - 88 | |
} |
Right now, every user has just ROLE_USER
. So next: let's start adding extra roles to some users in the database to differentiate between normal users and admins. We'll also learn how to check authorization rules in Twig so that we can conditionally render links - like "log in" or "log out" - in the right situation.
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")