Profile picture for user admin
Daniel Sipos
31 Jan 2017

Drupal 8 is very flexible when it comes to controlling access to your routes. It inherits quite a bit from the Symfony routing system, but adds its own flavour on top of that. In this article we are going to look at an example of a complex access requirement. In doing so, we won't cover the simpler use cases which are already described in the Drupal.org docs, but we will sure make use of some of them.

The requirement

So let's imagine this scenario: we have two types of users (employees and managers) whose persona is not determined by a user role. Let's say their "role" is determined on the fly as a result of an API call or some dynamic thing.

Now, let's say we have 3 routes: Route A (accessible for employees only), Route B (accessible for managers only) and Route C (accessible for both).

Finally, imagine we have a service called UserType which we can ask what type of person the current user is.

Implementation

One of the cool things about the Route access control in Drupal 8 is the ability, as the docs show, to delegate the access checking to a service. So a basic implementation for Route A and Route C can be something like this.

my_module.route_a:
  path: 'route-a'
  defaults:
    _controller: '\Drupal\my_module\Controller\DefaultController::buildRouteA'
    _title: 'Route A'
  requirements:
    _company_access_check_employee: 'true'

This is the route definition. As you can see, as per the docs, we have a requirement for the company_access_check access service to return the access result. So let's quickly see that service:

my_module.company_access_check:
  class:  Drupal\my_module\Access\CompanyAccessCheck
  arguments: ['@user_type']
  tags:
    - { name: access_check, applies_to: _company_access_check_employee }

A simple tagged service definition with a dependency to our fictitious UserType service that tells us the type of person the current user is. Additionally, we specify that this access checking service should be applied to all routes with the requirement _company_access_check_employee.

I am not going to show you this class because an example is already covered in the docs. However, it has one method called access() which by default gets passed the AccountInterface of the current user. So with the help of our UserType service we can determine whether the current user is an employee. Then we can return either AccessResult::forbidden() or AccessResult::allowed().

For managers, we do the same: create a new service and apply it to Route C.

So where does the complication come? Well, you guessed it: Route B which requires both. If we add two requirements to the route, let's say something like this:

my_module.route_b:
  path: 'route-b'
  defaults:
    _controller: '\Drupal\my_module\Controller\DefaultController::buildRouteB'
    _title: 'Route B'
  requirements:
    _company_access_check_employee: 'true'
    _company_access_check_manager: 'true'

It will check for both but grant access only if both return positive. So in our case this won't be very helpful since we need to check if the user is either. For the purposes of this article, please forgive the implication that managers are not also employees.

The solution

What we can do is create another access service called something like company_access_check_both which is responsible for determining if the current user is of one of the user roles. This is fine if our requirements are as simple as we described them. But what happens when we have multiple user types and a bunch of different routes where we have to mix and match the user types which have access to them? Creating a service for all these different types of combinations is not very efficient.

So instead, let's create a generic service called company_access_check_multiple AND specify in the route the type of user that has access to it in the form of a custom option. For example, the route definition can be something like this:

my_module.route_b:
  path: 'route-b'
  defaults:
    _controller: '\Drupal\my_module\Controller\DefaultController::buildRouteB'
    _title: 'Route B'
  requirements:
    _company_access_check_multiple: 'true'
  options:
    _company_access_users:
      - Employee
      - Manager

In this route we created a custom option called _company_access_users in which we list the types of users that should have access to it.

But how can we make use of this inside our service? Well, the Route object can be inspected and the list of allowed user types can be retrieved:

$types = $route->getOption('_company_access_users');

So if the route has that option, $types will tell us what type the current user needs to be in order to have access.

However, where do we get the Route object? As we know, the access() method of the service only receives the user account as a parameter. We might be tempted to inject the current route match service into our own. This does the trick, but only when the route in question is being checked upon a user actually going to it. It will miserably fail when a given route is being checked for access from another one (for example when building menu links).

If we dig deep and look closely, before our access() method is called, an arguments resolver is employed via the AccessArgumentsResolverFactory. This allows for the current user account to be passed to the access() method. But what not many people know is that if we type hint our access() method with either Route, RouteMatchInterface or Request, we will be getting those parameters as well. And in this case, the Route object is that of the route being checked for access rather than the current route.

So something like this:

public function access(AccountInterface $account, Route $route) {
  $types = $route->getOption('_company_access_users');
  // etc
}

So there you have it. A neat little trick that opens the door to some complex access restriction rules on your routes.

Profile picture for user admin

Daniel Sipos

CEO @ Web Omelette

Danny founded WEBOMELETTE in 2012 as a passion project, mostly writing about Drupal problems he faced day to day, as well as about new technologies and things that he thought other developers would find useful. Now he now manages a team of developers and designers, delivering quality products that make businesses successful.

Contact us

Comments

benellefimostfa 01 Feb 2017 15:22

A good Drupal 8 module to

A good Drupal 8 module to manage permissions: https://www.drupal.org/project/config_perms

devaanshu 26 Jul 2017 09:35

userType service

I am new to drupal and trying to do something which need to diffrentiate in between users, so can you please tell me how to build userType service

Anonymous 19 May 2020 13:06

Thanks

Thanks, man.
Whenever I'm looking for a solution for a slightly more complex problem with Drupal I somehow end up on your site.

Tobias 14 Oct 2020 18:52

Really such a great support by this article

Thank you so much - as the commenter before said your blog posts are really helpful whenever a more complex problem appears. Thumbs up!

Add new comment