Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Symfony3: Check if a route is accessible for a ROLE or a list of ROLES

I've implemented this in a current project as a service. The solution is straightforward but depends on security annotations. You may adapt the solution to fit your needs... It should be easy to use custom annotations or to combine this with configuration options running in from config.yml or a database.

This is related with Symfony Issue #6538

namespace Acme\Security\Roles;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Bridge\Monolog\Logger;
use Doctrine\Common\Annotations\AnnotationReader;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security as SecurityAnnotation;
use Symfony\Component\ExpressionLanguage\Expression;

class AcmePathRoles {
    
    protected $router;
    protected $accessDecisionManager;
    protected $providerKey;
    protected $userProvider;
    protected $filelogger;
    protected $annotationReader;

    public function __construct(RouterInterface  $router, AccessDecisionManagerInterface $accessDecisionManager, UserProviderInterface $userProvider, $providerKey, Logger $filelogger = null) {
        $this->router = $router;
        $this->accessDecisionManager = $accessDecisionManager;
        $this->userProvider = $userProvider;
        $this->providerKey = $providerKey;
        $this->filelogger = $filelogger;
        $this->annotationReader = new AnnotationReader();
    }
    
    /**
     * 
     * @param string $path
     * @param array $security array with roles, security expression, voter, etc.
     * @return array $roles
     */
    public function getRolesForPath($path, array $rolesToCheck, array $security){
        $request = Request::create($path,'GET'); 
        $allowed_roles = [];
        foreach($rolesToCheck as $role) {
            // You will need a function to load a user object. 
            // Out of the box there's no loadOneUserByRole function available
            $user = $this->userProvider->loadOneUserByRole($role);
            if ($user && $this->decideOnRoleUser($user, $security, $request, $role, $path)) {
                $allowed_roles[] = $role;
            }
        }
        return $allowed_roles;
    }

    /**
     * 
     * @param string $_route
     * @param string $routeparams
     * @param string $routecontroller
     * @param string $routemethod
     * @param array $security
     * @return boolean|array with roles
     */
    public function getRolesForRoute($_route, $route_params, $route_controller, $route_method, array $rolesToCheck, array $security){
        $route = $this->router->getRouteCollection()->get($_route);
        $params = array();        
        if ($route instanceof Route) {
            if ($route_params) {
                /* @var $route_params string */
                \parse_str($route_params, $params);
            }
            $security = $this->checkForSecurityAnnotation($route_controller, $route_method, $security);
            return $this->getRolesForPath($this->router->generate($_route, $params, RouterInterface::ABSOLUTE_PATH), $rolesToCheck, $security);
        }
        return false;
    }
    
    private function checkForSecurityAnnotation($routecontroller, $routemethod, array $security) {
        if ($routecontroller && $routemethod) {
            $reflectedClass = new \ReflectionClass($routecontroller);
            $annotations = $this->annotationReader->getMethodAnnotations($reflectedClass->getMethod($routemethod));
            foreach($annotations as $annotation) {
                if ($annotation instanceof SecurityAnnotation) {
                    $security = array(new Expression($annotation->getExpression()));
                    break;
                }
            }
        }                    
        return $security;
    }

    /**
     * 
     * @param User $user The user object with UserInterface implementation
     * @param array $security
     * @param Request $request
     * @param string $role A ROLE_SOMETHING
     * @param string $path
     * @return bool True if Role has access
     */
    private function decideOnRoleUser($user, array $security, Request $request, $role, $path) {
        $token = new UsernamePasswordToken($user, $user->getPassword(), $this->providerKey, $user->getRoles());
        $hasAccess = $this->accessDecisionManager->decide($token, $security, $request);
        //if (null !== $this->filelogger) {
        //    $this->filelogger->warning('Role allowed for path', array('role' => $role, 'path' => $path, 'hasAccess' => $hasAccess));
        //}
        return $hasAccess;
    }   
}

To use the class you have to register it as a service in service.yml:

services:
    app.path_roles:
        class: 'Acme\Security\Roles\AcmePathRoles'
        arguments: ['@router', '@security.access.decision_manager', '@app.user_provider', 'main', '@?logger']

If you don't provide your own UserProvider like I do you have to assure that you can load an example user object for a given role. (See comment in class code)

Use case: You have a page with a form and security restrictions apply and we want to inform via email about changes. We can now submit with the form some parameters like

  • app.request.attributes.get('_route')
  • app.request.attributes.get('_route_params')
  • app.request.attributes.get('_controller')

(Please note that the syntax is based on twig, you can access this within a controller and php as well)

_route and _route_params allow us to build a valid route to generate a path and _controller contains our controller class and the method used to serve the response. (You can also use some fake route_params because the controller action will not fire.) If you check the code you see that these values are used to check from within our service class for roles suitable to receive notifications, in this case via email for a given route / path. You can also use the class to test a path directly within a security context you have to supply. The security context accepts roles but also security expressions.

A final code example shows how I call this from my notification listener:

    /* @var Object $annotation Includes the data we need */ 
    list($controllerService, $controllerMethod) = explode('::', \urldecode($annotation->getController()));
    /* @var array $security Fall back security definition to establish decision */
    $security = ['ROLE_ADMIN','ROLE_SUPPORT']; // can be empty array as well or security expression packed into an array          
    $rolesToCheck = ['ROLE_USER', 'ROLE_EDITOR', 'ROLE_PEER', 'ROLE_SUPPORT', 'ROLE_ADMIN']
    $allowed_roles = $this->pathRoles->getRolesForRoute($annotation->getRoute(), $annotation->getRouteParams(), $controllerService, $controllerMethod, $rolesToCheck, $security);
@LouWii

This comment has been minimized.

Copy link

LouWii commented Aug 30, 2018

I think you can improve that part of the code

$annotations = $this->annotationReader->getMethodAnnotations($reflectedClass->getMethod($routemethod));
foreach($annotations as $annotation) {
    if ($annotation instanceof SecurityAnnotation) {

By doing

$annotation = $this->annotationReader->getMethodAnnotation($reflectedClass->getMethod($routemethod), SecurityAnnotation::class);

Unless there's a reason to go through all annotations of the method but I don't think so.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.