iamdto
iamdto

Reputation: 1372

Bind one route to different controllers depending on user roles

In my Symfony 2 app I have 3 different user roles that can have access to a backend administration 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'd like my app to display different informations depending on the user role, which means 3 controllers binding to an only route.

What's the best way to handle this ?

I was thinking about some solutions but none seems to be good for me :

  1. One controller, and in each action I just test 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 that does the job, IMO obviously that's not a good design.

  2. One BackendController that dispatch to 3 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.

  3. I tried to make controllers extends each others :

    <?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 it's a better design but I can't manage to make it works. The routing system stops on the first controller it matches. I'd like to make it act king of cascading style automatically (i.e. if user is staff then go to PostStaffController, otherwise if user is moderator go to PostModeratorController, otherwise go to PostAdminController).

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

I'm looking for the best designed and the more flexible solution has there's chance that we add more roles in the future.

Upvotes: 8

Views: 3870

Answers (4)

Łukasz Jakubek
Łukasz Jakubek

Reputation: 1013

IMHO, You sholdn't fire different controllers for the same route based on roles. It's just different responsibilities. Routes are for select controller, role are for privileges. After a year you will not remember the trick, ie. when you will trying add new role.

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

  1. When the controller for different roles is much different, I use different routes with redirect when needed.
  2. When the controller is similar but content is different, ie. different database query conditions, I use solution similar to yours 2. but instead forwading, use private/protected methods from the same controller to make the Job. There is one hack - You must check role from top to down, ie. first check ROLE_ADMIN, next ROLE_OPERATOR and last ROLE_STAFF, because when your ROLE_ADMIN inherit from ROLE_STAFF, then block for user catch it.
  3. When the difference is just in some blocks of information that should be shown/hide for different roles, I stay with one controller and check role in template to determine which block render or not.

Upvotes: 1

Jason Hendry
Jason Hendry

Reputation: 303

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

There is an option to replace the matcher_class which should be possible in config.yml.

If you subclass UrlMatcher and overRide matchRequest that will take precedence over the Path match (url only).

matchRequest takes a parameter $request (Request object)

The Request object should contain the user information provided the security provider listener runs before the router listener and allow you select the route by combining the URL and User Role. The Routes are stored in an array indexed by name so the names will need to be different.

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

In order to generate the urls with {{ path('post_index', {...}) }} you will also need to replace the subclass the URLGenerator and inject that into the Router with the generator_class option.

Upvotes: 0

ken
ken

Reputation: 5086

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

should do the trick, and make sure to inject security.context service

Upvotes: 0

prehfeldt
prehfeldt

Reputation: 2182

How about an automated version of your second solution? Like:

    // 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 don't have your setup I haven't tested the code above. Btw: what is the difference between the different method to post something?

Upvotes: 0

Related Questions