Buy
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:

... 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():

... 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:

... 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'):

... 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'):

... 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:

... 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!

  • 2018-03-27 Diego Aguiar

    Hey Cesar

    Nice question. I have an idea but it depends on how much security do you need. My idea is to use obfuscation by creating a folder per each user and giving it a very hard guessing name, so only you can find those folders and files.
    But, if what you need is something more bullet proof, then you can give it a try to this solution: https://stackoverflow.com/a...

    Cheers!

  • 2018-03-27 Cesar

    Hi guys. I just made my first web with login access. How I can secure the images ? I only want that the user gets access to some images after login. I just need to move the images to another folder or do you have a better tip? I hope you can help me.

  • 2018-01-03 Victor Bocharsky

    Hey John,

    As always, security is a complex stuff, but looks like you nailed it ;)
    I'm glad to hear it helped you!

    Cheers!

  • 2018-01-02 John

    Hi Victor,

    I think after a few hours I've reimplemented what is probably in FOSUserBundle (although I've not gone and looked, and I've probably done it less well). I've created an event subscriber, and am using that to dynamically reload roles and linked it to kernel.controller - based on the group they are acting as a member of. So that deals with a 'group change' whether through a planned action, or through editing the browser bar.

    It broke the web profiler for a bit, until I worked out how to exclude triggering my event when the web profiler controller ran.

    so all good now, but it was an interesting journey.

  • 2018-01-02 Victor Bocharsky

    Hey John,

    Nice catch about voters! Yeah, voters are great and very flexible, and looks like it's exactly what you need if you want to flexibly secure something in your app, but the ROLE_* functionality isn't enough for you. However, there's also a concept about groups, if you use FOSUserBundle - check their docs to find more information and use cases about groups:
    https://symfony.com/doc/mas...

    P.S. If you don't use FOSUserBundle, well, you can get the idea of roles and easily implement them by yourself, just steal some code from the bundle ;)

    Cheers!

  • 2017-12-31 John

    OK - ignore me, wrong question, wrong place.. It's voters isn't it?

    Thanks for some of the best courses anywhere.

  • 2017-12-30 John

    Hi Guys,

    I'm building a web app where users can log in as belonging to different groups.. Currently roles are global - but will end up being group specific but that's for another day.

    Today I'm try to ensure that manually switching the group id in the browser bar is blocked in the controller, and I think @security annotations is the way to go. I've checked the symfony docs, and since there are suspicious uses of the name 'Ryan' I'm assuming you guys wrote them.

    Can you give me any hints please for the best way of checking against a one to many table linked to the user table - where the one to many linked table has additional characteristics - so can't be handled invisibly by Doctrine?

    I'm thinking I need to create a new repository function for this particular query - but I'm not quite sure how the expression engine syntax will reference it.

    Thanks,
    John

  • 2017-12-11 Diego Aguiar

    NP! you are welcome, and I'm glad to hear that you could fix your problem :)

  • 2017-12-10 Yahya A. Erturan

    Diego Aguiar I double checked everything. It seemed fine. However, finally it turned out a bug in filesystem of Vagrant caused some missing files. I remove the dependencies then install again, It works now. Happy that we can ask you so we do not go forward onto darkside (giving up :)). Thanks.

  • 2017-12-08 Diego Aguiar

    Hey Yahya A. Erturan
    This may sound silly but did you add the correct use statement for Security annotation?


    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

    Cheers!

  • 2017-12-08 Yahya A. Erturan

    Hi weaverryan,

    I can't use @Security("") :( I am using symfony 3.4 LTS and
    I have already run:
    $ composer require sensio/framework-extra-bundle
    $ composer require symfony/expression-language v3.4.1

    But now when I want to add method-wide security like "@Security("is_granted('ROLE_ADMIN')")" it gives an error:

    1/1) ClassNotFoundException
    Attempted to load interface "ExpressionFunctionProviderInterface" from namespace "Symfony\Component\ExpressionLanguage".
    Did you forget a "use" statement for another namespace?
    in ExpressionLanguageProvider.php (line 22)

    What should I do?

  • 2017-08-17 Mike

    Perfect explanation! Ive seen now that start() is calling getLoginUrl() and there it is, the reason thy it redirects to /login:

        protected function getLoginUrl()

    {

    return $this->router->generate('security_login');

    }

    Many thanks Ryan!

  • 2017-08-15 weaverryan

    Yo Mike!

    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

    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....