Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Denying Access in a Controller

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

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

The AccessDeniedException

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.

Denying Access with IsGranted Annotation/Attribute

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!');
    }
    // ...
}

Denying Access to an Entire Controller Class

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.

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 1 year ago | 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!

This tutorial also works great for 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