Buy

I do use access controls to lock down big sections, but, mostly, I handle authorization inside my controllers.

Deny Access (the long way)!

Let's play around: comment out the access_control:

36 lines app/config/security.yml
... lines 1 - 2
security:
... lines 4 - 33
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }

And open up GenusAdminController. To check if the current user has a role, you'll always use one service: the authorization checker. It looks like this: if (!$this->get('security.authorization_checker')->isGranted('ROLE_ADMIN'). So, if we do not have ROLE_ADMIN, then throw $this->createAccessDeniedException():

85 lines src/AppBundle/Controller/Admin/GenusAdminController.php
... lines 1 - 13
class GenusAdminController extends Controller
{
... lines 16 - 18
public function indexAction()
{
if (!$this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')) {
throw $this->createAccessDeniedException('GET OUT!');
}
... lines 24 - 31
}
... lines 33 - 84
}

That message is just for us developers.

Head back and refresh. Access denied!

So what's the magic behind that createAccessDeniedException() method? Find out: hold Command and click to open it. Ah, it literally just throws a special exception called AccessDeniedException:

398 lines vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php
... lines 1 - 21
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
... lines 23 - 38
abstract class Controller implements ContainerAwareInterface
{
... lines 41 - 257
/**
* Returns an AccessDeniedException.
*
* This will result in a 403 response code. Usage example:
*
* throw $this->createAccessDeniedException('Unable to access this page!');
*
* @param string $message A message
* @param \Exception|null $previous The previous exception
*
* @return AccessDeniedException
*/
protected function createAccessDeniedException($message = 'Access Denied.', \Exception $previous = null)
{
return new AccessDeniedException($message, $previous);
}
... lines 274 - 396
}

It turns out - no matter where you are - if you need to deny access for any reason, just throw this exception. Symfony handles everything else.

Deny Access (the short way)!

Simple, but that was too much work. So, you'll probably just do this instead: $this->denyAccessUnlessGranted('ROLE_ADMIN'):

83 lines src/AppBundle/Controller/Admin/GenusAdminController.php
... lines 1 - 13
class GenusAdminController extends Controller
{
... lines 16 - 18
public function indexAction()
{
$this->denyAccessUnlessGranted('ROLE_ADMIN');
... lines 22 - 29
}
... lines 31 - 82
}

Much better: that does the same thing as before.

Denying Access with Annotations

And, I have another idea! If you love annotations, you can use those to deny access. Above the controller, add @Security() then type a little expression: is_granted('ROLE_ADMIN'):

83 lines src/AppBundle/Controller/Admin/GenusAdminController.php
... lines 1 - 7
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
... lines 9 - 14
class GenusAdminController extends Controller
{
/**
* @Route("/genus", name="admin_genus_list")
* @Security("is_granted('ROLE_ADMIN')")
*/
public function indexAction()
{
... lines 23 - 29
}
... lines 31 - 82
}

This has the exact same effect - it just shows us a different message.

Locking down an Entire Controller

But no matter how easy we make it, what we really want to do is lock down this entire controller. Right now, we could still go to /admin/genus/new and have access. We could repeat the security check in every controller... or we could do something cooler.

Add the annotation above the class itself:

83 lines src/AppBundle/Controller/Admin/GenusAdminController.php
... lines 1 - 7
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
... lines 9 - 11
/**
* @Security("is_granted('ROLE_ADMIN')")
* @Route("/admin")
*/
class GenusAdminController extends Controller
{
... lines 18 - 82
}

As soon as you do that, all of these endpoints are locked down.

Sweet!

Leave a comment!

  • 2017-08-15 weaverryan

    Yo Mike P!

    When you try to access a protected page as an anonymous user (e.g. /admin/genus), then the "entry point" of your firewall is called to determine what to do next. What the heck is an entry point? Well, each "method" of authentication - e.g. each guard authenticator of if you're using some core authentication methods, then each thing like form_login or http_basic - has an "entry point" - a method that simply returns what Response should be sent to the user in this situation. In our authenticator, the start() method is the entry point.

    That's a long way of saying: when you go to /admin/genus as an anonymous user, then the start() method in your authenticator should be called and IT is determining what do send back to the user (in this case, it's redirecting to /login). But, if you have multiple authenticators or 1 authenticator and 1 other authentication mechanism (like form_login), then the entry point is only used from *one* of these. If I remember correctly, the start() method from your authenticator should win over the one in form_login. Let me know if this is your situation - other wise this last rambling is irrelevant :).

    Cheers!

  • 2017-08-15 Mike P

    For me it always redirects to /login if I try to access /admin/genus.
    I access SF 3.3.6 via https://aquanote.local/app_dev.php/admin/genus. Maybe its a new behavior? If I remove the @SECURITY line I can access the site with anon. User.

  • 2017-07-25 Victor Bocharsky

    Hey maxii123 ,

    Thanks for this tip ;)

    Cheers!

  • 2017-07-24 maxii123

    It's probably worth pointing out that when you do the top level annotation then all routes are relative to that eg
    /**
    * @Route("/people")
    * @Security("is_granted('ROLE_USER')")
    */

    class PersonController extends VollFilmController
    {

    /**
    * @Route("/",name="people_list")
    **/

    Here people_list url is actually /people

  • 2017-01-16 Teolan

    Thanks Victor!

  • 2017-01-16 Victor Bocharsky

    Yo Teolan,

    Actually, $credentials can be anything you want, but except the null value. If getCredentials() returns a non-null value, then the getUser() method is called and its return value is passed here as the $credentials argument. So from getCredentials() method you can return just a token string, or an array of entered username and password by user - no matter, you just have to handle this credentials in getUser method. That's why getUser() method doesn't have a typehint for $credentials - it can be any type. So as you see there's no magic ;)

    You can see the description of these methods here: http://symfony.com/doc/curr...

    Cheers!

  • 2017-01-16 Teolan

    HI Rayan,

    in the Class "LoginFormAuthentificator" there is a function named: setUser($credentials, ..)
    where is variable $credentials declared? (witch data-type?)
    I see in the interface doc that $credentials is a return value from getCredentials() but return variable name is $data and $credentials has in interface also no data-type

    is there a magic? :)

    Thanks a lot,
    Cheers!
    Teo

  • 2016-12-26 Victor Bocharsky

    Hey Boran,

    At first, I see that we can't change this message with @Security annotation, but users won't see this "Expression "has_role('ROLE_USER')" denied access" message in production anyway - you see it only in dev mode. So the better and *easiest* option here is to override the default `error403.html.twig` template (or just error.html.twig for all errors) and customize this page for your users. Please, check out this example: https://knpuniversity.com/s... or look into the docs: https://symfony.com/doc/cur...

    Cheers!

  • 2016-12-26 Boran Alsaleh

    I'm trying to use @Security annotations for my routes. Like this:

    /**
    * @return Response
    * @Route("/action")
    * @Security("has_role('ROLE_USER')")
    * @Template()
    */public function someAction(){
    return array();}

    When the security restriction fires an exception, I get the message Expression "has_role('ROLE_USER')" denied access.

    This is not acceptable to be shown to the end user, so I'm trying to find a way to customize the message for annotation.

    Simple workaround is to not to use @Secutity annotations and write code like these:

    /**
    * @return Response
    * @Route("/action")
    *
    * @Template()
    */public function someAction(){
    if (!$this->get('security.context')->isGranted('ROLE_USER')) {
    throw new AccessDeniedException('You have to be logged in in
    order to use this feature');
    }

    return array();}

    But this is less convenient and less readable.

    Is it possible to write custom message to @Security annotations?

  • 2016-12-26 Victor Bocharsky

    Hey Boran,

    Could you explain a bit what're you trying to do? The @Security annotation throws exception like AccessDeniedHttpException, so users will see an Access Denied error with 403 status code. And you can override this Twig template to show a custom error page for this error. I hope it helps.

    Cheers!

  • 2016-12-24 Boran Alsaleh

    Hi Rayan ,

    Is it possible to write custom message to @Security annotations? If not how i can do it

    Best Regards ,

  • 2016-11-07 weaverryan

    Yep, I've done that before ;). Glad it helped and good luck!

  • 2016-11-05 Roel Beckers

    Yo Ryan!

    Thanks for the debug:router tip! At the top of my UserController, I somehow added @Route("/user") which turned my routes into /user/user/create. Simply removed this and now I'm nicely redirected to /login when I try to access /user/create with an unauthenticated user.

    Thanks a bunch!

    Cheers,
    R.

  • 2016-11-05 weaverryan

    Hey Roel!

    Hmm! So, a couple of things to try here:

    1) The "404 No Route Found GET /user/create" literally means what it says: this error has nothing to do with security or being denied access. It is actually saying (independent of security) "I don't see any route for /user/create". Try running a bin/console debug:router to see what URLs you have - something must be slightly misconfigured here in your routing configuration.

    2) Once we have (1) figured out (first priority!) then we can "lock" things down with security. The feature that Victor is talking about is mentioned here (it's an older, Symfony 2 tutorial - but this hasn't changed): http://knpuniversity.com/sc.... Make sure to remove that access_denied_urls thing - that's not needed. Your firewall should look a bit like what Victor posted (https://knpuniversity.com/s... except after anonymous, you will configure whatever login mechanisms you need (e.g. form_login, guard, http_basic, whatever). You won't have anything under your firewall that actually denies access in any way - that'll happen under access_control. Once you have this, an anonymous user who tries to visit *any* page that requires login will automatically be redirected to /login: that's a natural feature of Symfony (of course, you can configure exactly where they're redirected: but Symfony always redirects the user to the "login" page if it detects that an anonymous user is trying to access a protected page).

    Let us know if that helps! And cheers back!

  • 2016-11-05 Roel Beckers

    Thanks! I've added these lines now to my main firewall in security.yml. Still get the 'no route found for "GET /user/create" error. The access_control list is currently empty.

    Cheers!

  • 2016-11-03 Victor Bocharsky

    Great! That's the point I think. So you need to authenticate user on every page, for it you need to specify "pattern: ^/" in the main firewall:


    security:
    firewalls:
    main:
    pattern: ^/
    anonymous: true

    Do you have these lines in your firewall? Also what's in your access_control list of security.yml?

    Cheers!

  • 2016-11-03 Roel Beckers

    Hi Victor,

    I've added this to the top of my UserController:

    /**
    * @Security("is_granted('ROLE_HR')")
    * @Route("/user")
    */

    class UserController extends Controller

    When a go to /user/create with an un-authenticated user, the web debug toolbar shows me "n/a - You are not authenticated".

    Cheers,
    Roel

  • 2016-11-03 Victor Bocharsky

    No, it's something different. How do you restrict access to this page? Is your anonymous user authenticated on /user/create page? You can check it in web debug toolbar, you should see there:

    Logged in as: anon.
    Authenticated: Yes
    Token class: AnonymousToken

    Cheers!

  • 2016-11-02 Roel Beckers

    Hi Victor,

    Yes, I tried it. When I browse as an anonymous user to /user/create I get a 404 No route found for "GET /user/create".
    In my security.yml I've added 'access_denied_url: /login' in firewalls > main. Is this the out-of-the-box feature you're referring to?

    Cheers,
    R.

  • 2016-11-02 Victor Bocharsky

    Hey Roel,

    Have you tried it? Actually, Symfony has this feature out-of-the-box! :) You just need to properly configure your firewall in security.yml file.

    Cheers!

  • 2016-11-01 Roel Beckers

    Hi Ryan, me again :)

    The web app I'm creating will only allow authenticated users. Is there a way when a non-authenticated user tries to reach e.g. /user/create he is automatically redirected to /login?

    Cheers and thanks already!
    Roel

  • 2016-09-22 Yang Liu

    holy shit... of course... the best(worst) ones are always the typos... I would never found it by myself o.O

  • 2016-09-21 weaverryan

    Ah, it's such a small detail! In your security.yml, you have ROLE_AUHTOR - it's a typo, should be ROLE_AUTHOR.

    I bet that'll do it :)

    Cheers!

  • 2016-09-20 Yang Liu

    so, now in my security.yml:

    security:
    role_hierarchy:
    ROLE_AUHTOR: [ROLE_WRITE_POST, ROLE_WRITE_COMMENT]
    ROLE_USER: [ROLE_WRITE_COMMENT]

    (I checked here, its 4 spaces indented)

    and in my twig:

    {% if is_granted('ROLE_WRITE_POST') %}
    New Post
    {% endif %}
    however, when I log in with a user with 'ROLE_AUTHOR', I can't see the button.
    Just for testing,

    {% if is_granted('ROLE_WRITE_POST') or is_granted('ROLE_AUTHOR') %}
    New Post
    {% endif %}
    this is working fine. So, did I forget anything?

  • 2016-09-19 Victor Bocharsky

    Ah, you can see right bracket ")" at the end of that link. Actually, that was a part of Ryan context that Disqus mistakenly regarded as a part of link. Just remove it and it will work :)

    P.S. I fixed the link.

  • 2016-09-16 weaverryan

    Hey Yang!

    Another good question :). Here's what I would do in Twig:


    {% if is_granted('ROLE_ADD_COMMENT') %}

    Then, you can use the role_hierarchy feature ( https://knpuniversity.com/s... ) to give ROLE_USER and ROLE_AUTHOR this role:


    role_hierarchy:
    ROLE_USER: [ROLE_ADD_COMMENT]
    ROLE_AUTHOR: [ROLE_ADD_COMMENT]

    This should help keep things cleaner :).

    And also, though you don't need it yet, you may eventually want to use voters: https://knpuniversity.com/s.... This is needed when the access decision you need to make depends on some data - e.g. deciding whether or not a user can *edit* a blog post, which in your app, might be allowed only if the author if the blog post matches the current user. So, roles are for global permissions - "Can the user do ABC in general?" - but if you need object-specific permissions - "Can the user EDIT this specific object" - then look into voters.

    Cheers!

  • 2016-09-16 Yang Liu

    Hello Ryan,
    me again. Thanks first for all the nice tutorials, they help me a lot lerning symfony.
    This section is all about denie/grant a ROLE to access a whole homepage. My question:
    e.g. I have different roles for my blog, if not logged in, you can only read post/comments. ROLE_USER could read post/comments and post comments, ROLE_AUTHOR can read, post comments AND write posts. What I am planing right now is, logged in authors would see a button "write post" when "normal" users don't. I could do this (similar to login/logout button) by writing something like
    {% if is_granted('ROLE_AUTHOR') %}
    Write Post
    {% endif %}

    and I could do the same with comments
    {% if (is_granted('ROLE_AUTHOR') or is_granted('ROLE_USER')) %}
    {% include 'formwhereIcanwriteacomment.html.twig' %}
    {% endif %}

    would this work? Is there a better way to do such authorizations?

    thx and have a nice weekend

    edit: small question: I want to test my theory and add some fixtures, I remember from the beginner tutorial, you used '50% ryan.jpeg: leanna.jpg', I tried
    roles: '50%? ['ROLE_AUTHOR'] : ['ROLE_USER']'
    but single-quote won't work here, I tried double quote:
    roles: "50%? ['ROLE_AUTHOR'] : ['ROLE_USER']"
    this seems to put a string into database so I got
    "Warning: in_array() expects parameter 2 to be array, string given" at the getRoles()-method.
    What should I do?

    edit2: solved the "small question" by providing a roles() function in LoadFixtures-class, which just random returns ROLE_USER or ROLE_AUTHOR as an array.

  • 2016-08-01 Victor Bocharsky

    Hey Andjii,

    This class was added in the previous episode Symfony Forms: Build, Render & Conquer! of "Starting in Symfony 3!" series. We started working with it at the beginning - check https://knpuniversity.com/s... .

    Cheers!

  • 2016-08-01 Andjii

    where did we get adminController? what the part was about it? I am afraid, I've missed it....