Bind one route to different controllers depending on user role

In my Symfony 2 application, I have 3 different user roles that can access the backend control part:

role_hierarchy: ROLE_STAFF: ROLE_USER ROLE_MODERATOR: ROLE_STAFF ROLE_ADMIN: ROLE_MODERATOR 

For a route like http://example.org/admin/post/ , I would like my application to display different information depending on the role of the user, which means binding 3 controllers to the route only .

What is the best way to handle this?

I was thinking of some solutions, but nothing seems good to me:

  • One controller, and in each action I just check the user role:

     <?php /** * @Route("/admin/post") */ class PostController extends Controller { /** * Lists all post entities. * * @Route("/", name="post_index") * @Template() * @Secure(roles="ROLE_STAFF") */ public function indexAction() { $user = $this->get('security.context')->getToken()->getUser(); if ($this->get('security.context')->isGranted('ROLE_STAFF')) { // Do ROLE_STAFF related stuff } else if ($this->get('security.context')->isGranted('ROLE_MODERATOR')) { // Do ROLE_MODERATOR related stuff } else if ($this->get('security.context')->isGranted('ROLE_ADMIN')) { // Do ROLE_ADMIN related stuff } return array('posts' => $posts); } } 

    Even if this does the job, IMO it is obvious that this is not a good design.

  • One BackendController that sends three different controllers:

     <?php /** * @Route("/admin/post") */ class PostBackendController extends Controller { /** * Lists all post entities. * * @Route("", name="admin_post_index") * @Template("AcmeBlogBundle:PostAdmin:index.html.twig") * @Secure(roles="ROLE_STAFF") */ public function indexAction() { if ($this->get('security.context')->isGranted('ROLE_STAFF')) { $response = $this->forward('AcmeBlogBundle:PostStaff:index'); } else if ($this->get('security.context')->isGranted('ROLE_MODERATOR')) { $response = $this->forward('AcmeBlogBundle:PostModerator:index'); } else if ($this->get('security.context')->isGranted('ROLE_ADMIN')) { $response = $this->forward('AcmeBlogBundle:PostAdmin:index'); } return $response; } } 

    Same as number one.

  • I tried to get the controllers to expand each other:

     <?php /** * @Route("/admin/post") */ class PostStaffController extends Controller { /** * Lists all post entities. * * @Route("/", name="post_index") * @Template() * @Secure(roles="ROLE_STAFF") */ public function indexAction() { $user = $this->get('security.context')->getToken()->getUser(); // Do ROLE_STAFF related stuff return array('posts' => $posts); } } <?php /** * @Route("/admin/post") */ class PostModeratorController extends PostStaffController { /** * Lists all post entities. * * @Route("/", name="post_index") * @Template() * @Secure(roles="ROLE_MODERATOR") */ public function indexAction() { $user = $this->get('security.context')->getToken()->getUser(); // As PostModeratorController extends PostStaffController, // I can either use parent action or redefine it here return array('posts' => $posts); } } <?php /** * @Route("/admin/post") */ class PostAdminController extends PostModeratorController { /** * Lists all post entities. * * @Route("/", name="post_index") * @Template() * @Secure(roles="ROLE_ADMIN") */ public function indexAction() { $user = $this->get('security.context')->getToken()->getUser(); // Same applies here return array('posts' => $posts); } } 

    IMO is the best design, but I can't get it to work. The routing system stops at the first controller that it matches. I would like it to automatically execute cascading style rules (i.e. if the user is a staff, then go to the PostStaffController, otherwise, if the moderator user goes to PostModeratorController, otherwise go to PostAdminController).

  • Add a listener to kernel.controller in my BlogBundle that will do the same job as number 2?

I am looking for a better design, and a more flexible solution has a chance that we will add more roles in the future.

+8
symfony
source share
4 answers

IMHO, you did not shoot at different controllers for the same role-based route. These are different responsibilities. Routes for the selected controller, role for privileges. After a year you don’t remember the trick, i.e. when you try to add a new role.

Of course, the problem of different content for different roles is quite common, so my favorite solutions in this case are:

  • When the controller for different roles is very different, I use different routes with redirection when necessary.
  • When the controller is similar, but the content is different, i.e. different conditions for database queries, I use a solution similar to your 2. Instead, instead of using forwading, use private / protected methods from the same controller to complete the job. There is one hack - you have to check the role from top to bottom, i.e. first check ROLE_ADMIN, the next ROLE_OPERATOR, and the last ROLE_STAFF, because when your ROLE_ADMIN inherits from ROLE_STAFF, then block it for the user.
  • When the difference is only in some blocks of information that should be shown / hidden for different roles, I stay with one controller and check the role in the template to determine which rendering block or not.
+1
source share

What about the automatic version of your second solution? How:

  // Roles ordered from most to least significant (ROLE_ADMIN -> ROLE_MODERATOR -> etc) $roles = $myUserProvider->getRoles(); foreach ($roles as $role) { // add a check to test, if the function you're calling really exists $roleName = ucfirst(strtolower(mb_substr($role, 0, 5))); $response = $this->forward(sprintf('AcmeBlogBundle:Post%s:index', $roleName)) break; } // Check that $response is not null and do something with it ... 

Since I do not have your setup, I have not tested the code above. Btw: what is the difference between another method for posting something?

0
source share

see http://symfony.com/doc/current/book/internals.html#kernel-controller-event

should do the trick and be sure to enable the security.context service

0
source share

in vendor/symfony/symfony/src/Symfony/Component/Routing/Router.php

It is possible to replace matcher_class , which should be possible in config.yml .

If you are a subclass of UrlMatcher and overRide matchRequest , which will take precedence over path matching (URL only).

matchRequest accepts the parameter $ request (request object)

The Request object should contain the user information provided to the security listener before starting the receiver of the router and allow you to choose the route by combining the URL and the user role. Routes are stored in an array indexed by name, so the names must be different.

You could use names like post_index[USER] post_index[STAFF] post_index[MODERATOR]

To generate URLs using {{ path('post_index', {...}) }} , you will also need to replace the URLGenerator subclass and insert it into the router using the generator_class option.

0
source share

All Articles