Controlling Access to Drupal 8 Routes with Access Checks

In the previous post, I looked at how to put together a basic route controller in Drupal 8, and restrict access by specifying permissions. But there are my situations where basic permissions aren't enough.

In Drupal 7 we had procedural access callbacks. In Drupal 8 we now have AccessCheck services.

This post takes you through how to use the new AccessCheck interface to provide a custom access checker for your routes.

Controlling Access with AccessCheck

In Using Drupal 8's new route controllersour previous example, we are only checking that a user has a certain permission to access the route. What if we have some custom logic to check? In Drupal 7 we would use an access callback. In Drupal 8 we need to create an implementation of AccessCheckInterface and define it as a service in the dependency injection container.

The first step is to modify our route configuration, and specify a special tag for our access callback in our trousers.routing.yml file. Let's assume our Trousers access check needs to assert that the user has legs.

In trousers.routing.yml file:

trousers_list:
  pattern: '/trousers'
  defaults:
    _content: '\Drupal\trousers\Controller\TrouserController::list'
  requirements:
    _access_trousers_has_legs: 'TRUE'

In the above, we have created a new requirement, _access_trousers_has_legs: TRUE

The next step is to create our service definition for our TrousersHasLegsAccessCheck class by adding the following in trousers.services.yml in the root directory of our module.

services:
  access_check.trousers_has_legs:
    class: Drupal\trousers\Access\TrousersHasLegsAccessCheck
    tags:
      - { name: access_check }

The above snippet defines a new service with the unique key access_check.trousers_has_legs. By convention, we name any service that is an access check with an access_check. prefix. Next we define the class that implements our AccessCheckInterface. And finally, we tag the service with access_check so Drupal can find it when it loads all access checks.

Like all Drupal 8 classes loaded with PSR-0 namespaces, we create our access check in a directory that matches the namespace. In our trousers module, this is trousers/lib/Drupal/trousers/Access/TrousersHasLegsAccessCheck.php

Our TrousersHasLegsAccessCheck.php access check class looks like this.

<?php

namespace Drupal\trousers\Access;

use Drupal\Core\Access\StaticAccessCheckInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
class TrousersHasLegsAccessCheck implements StaticAccessCheckInterface {

  public function appliesTo() {
    return '_access_trousers_has_legs';
}
 
public function access(Route $route, Request $request, AccountInterface $account) {
    if (!$account->hasLegs()) { // check if a user has legs
      return static::DENY; // denied! No legs.
    }
    return static::ALLOW; // OK to access trousers
  }
}

The appliesTo() method checks to see whether this access check should apply. It does this by simply checking whether the current route has an _access_trousers_has_legs requirement. As we defined this in our trouser.routing.yml file earlier, this should match when our route gets fired.

The access() method is where all the action happens. It gets passed the current Route, Request and current user (AccountInterface) objects. As this access check is a service, you could also inject in a database connection here to do some custom lookups. (See Using Drupal 8's new route controllers for an example of dependency injection).

There are some useful contstants on the StaticAccessCheckInterface interface (e.g DENY, ALLOW) which you return to let the router component know whether to allow or deny access. As you can see, we only allow access to the trousers route, if the current user has legs.

There are more advanced access rules that I haven't gone into here, like combining multiple access checks on the same route.

Please feel free to post comments or questions!

UPDATE: AccessInterface now passes current user (AccountInterface) as a parameter, so we don't need to look it up!

Drupal 8 Routing
Back to top

Comments

Posted by rszrama | August 1, 2013

A helpful follow-up might be a clarification or succinct definition of what a "service" as defined in a *.services.yml file actually represents. Because of my experience with the Services module, my first assumption was that these were somehow related to the REST module, but as it turns out they're more like types of classes a module might define to do something on behalf of another module.

But I'm not sure that really gets at it... and I haven't yet got to a part of the Symfony tutorial that covers this stuff. : P

Posted by Berdir | August 6, 2013

@rszrama: Yes, that's basically it. A service is a class that you can instantiate and call methods on it. The main advantage of defining it as a service is that the service container takes care of your dependencies. Your service might need the database, cache and some other services, you define that in the services.yml file and when requested, your class is instantiated, those other services are too if necessary and passed into your class.

See https://drupal.org/node/1539454 for the main change notice

Posted by martin frances | October 30, 2013

Minor correction for you folks following at home

I am getting errors saying return value from TrousersHasLegsAccessCheck:access - must be a string so

_access_trousers_has_legs: TRUE

becomes

_access_trousers_has_legs: 'TRUE'

yuck

kim's picture
Posted by kim | October 30, 2013

Thanks Martin. I've updated the code sample.

kim's picture
Posted by kim | October 31, 2013

AccessInterface has been updated and now the current user (AccountInterface $account) is passed as a parameter to the access() method. I've updated the code sample to reflect this.

Posted by Alex Weber | May 2, 2014

This is really cool! Here's the official change record: https://drupal.org/node/1851490Too bad multiple access checkers of the same type can't be stacked... it would be awesome to be able to check for multiple permissions without having to write a custom access checker...

Posted by matslats | June 29, 2014

Note that the appliesTo function has changed. Here is an example from views:
<code>
public function applies(Route $route) {
return $route->hasDefault('view_id');
}</code>

Posted by dpi | January 29, 2015

The link to "Using Drupal 8's new route controllers" is not invalid.

kim's picture
Posted by kim | January 30, 2015

Thanks @dpi. Fixed.

Posted by Michael Welford | February 26, 2016

You now need to return an AccessResult

Post a comment