Custom route access in Drupal 8 based on field values


Some custom routes are applicable to the same type and bundle of entity but aren’t necessarily available for different entities based on their data or field values. For instance, we have a project that uses a hierarchical group structure where each group entity is an “organization” bundle and the “level” of the hierarchy is stored in a “field_level” field. We have several custom routes that are available for different levels in the hierarchy. For example, you might want to view students in a “school” organization, but you would want to see a list of school sub-groups for a “district” organization. To handle these scenarios, we built a custom field value access checker.

This custom access checker evaluates a field for a particular value on any entity parameter.

Let’s create a custom module and build an access checker so we can see how it works.


Creating a custom module for the access checker


In your Drupal 8 site’s web/modules directory, create the directory custom/access_check_field_value and add access_check_field_value.info.yml:

name: 'Access check by field value'
description: 'Provides custom route access checking by a parameter specifying a field value.'
package: 'Access checking'
type: module
core: 8.x

Creating a custom access checker class


To create a custom access checker, we’ll need a class to execute our custom access check code. Create a directory web/modules/custom/access_check_field_value/src/Access and in that directory, create FieldValue.php.

<?php

namespace Drupal\access_check_field_value\Access;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\Routing\Route;

/**
* Determines whether the entity in the parameter has the given field value.
*
* @package Drupal\access_check_field_value\Access
*/
class FieldValue implements AccessInterface {

  /**
  * Access based on whether the current user owns the specified class.
  *
  * @param \Symfony\Component\Routing\Route $route
  * The route.
  * @param \Drupal\Core\Routing\RouteMatchInterface $routeMatch
  * The route matcher.
  *
  * @return   \Drupal\Core\Access\AccessResultAllowed|\Drupal\Core\Access\AccessResul  tForbidden
  * The access result.
  */
  public function access(Route $route, RouteMatchInterface $routeMatch)   {
    $parameter = $route->getRequirement('_field_value_parameter');
    $field = $route->getRequirement('_field_value_field');
    $value = $route->getRequirement('_field_value_value');
    $negate = $route->getRequirement('_field_value_negate');

    $entity = $routeMatch->getParameter($parameter);
    if ($entity instanceof EntityInterface) {
      $actualValue = $entity->hasField($field) ? $entity->get($field)->value : NULL;
      if ($negate && $actualValue != $value) {
        return AccessResult::allowed();
      }
      elseif (!$negate && $actualValue == $value) {
        return AccessResult::allowed();
      }
    }
    return AccessResult::forbidden();
  }

}

The access function gets the requirements we need, then makes sure the entity route parameter is actually an entity object. Then it uses the field name defined in _field_value_field to get the field value and stored that in $actualValue. If _field_value_negate is true, then it returns an allowed result if the actual value and the specified value don’t match. If _field_value_negate is false, then it returns an allowed result if the actual value and the specified value do match. If the field value isn’t what we expect or negate, then the access checker returns forbidden for the route.

AccessInterface requires that we implement the access() function, which will return the access result after evaluating the route parameters and specified field values. This access checker adds a few parameters we can use in our custom routes:

The _field_value_parameter parameter lets our function know what route parameter to look at and get a field value for.

The _field_value_field parameter identifies which field the access checker should look at.

The _field_value_value parameter defines the value that the access checker looks for.

The _field_value_negate determines whether a match or non-match will return an allowed result.

We’ll see in a moment how to define these requirements on a custom route.

The access() method has a few default arguments available that Drupal will give us if the arguments are properly type-hinted:

\Symfony\Component\Routing\Route $route provides the route object. We want this so we can access the route requirements and parameter definitions.

\Drupal\Core\Routing\RouteMatch $route_match provides the Drupal route match service. We want this so we can get the actual route parameter values.

\Drupal\Core\Session\AccountProxyInterface $account provides the account proxy class for the currently logged in user. Since our conditions only care about the entity identified in _field_value_parameter, the current user doesn’t matter in this case. We don’t need this for what we’re evaluating, so we’ll leave this argument out.


Creating a custom access checker service definition


Next, you’ll need to create a services.yml file. Create the file access_check_field_value.services.yml and add the following service definition:

services:
  access_check_field_value.field_value:
    class: Drupal\access_check_field_value\Access\FieldValue
    tags:
      - { name: access_check, applies_to: _field_value_field }

We named the service access_check_field_value.field_value and pointed it to the class we just created. At minimum, an access checker requires a _name tag of “access_check” and an applies_to value for the access requirement we implement (in this case, “_field_value_field” will let Drupal know we are using our access_check_field_value.field_value service whenever a route has a “_field_value_field” requirement).


Creating a custom module with a custom route using the access checker


I like to leave the access checker in its own module so the module only does one thing. To that end, let’s create a second module that will use our new access checker in a custom route. Create a new module directory at web/modules/custom/route_with_custom_access and in that directory, create route_with_custom_access.info.yml:

name: 'Route with custom access'
description: 'Provides custom route with access checking.'
package: 'Access checking'
type: module
core: 8.x

Then create route_with_custom_access.routing.yml.

route_with_custom_access.node_field:
  path: '/node/{node}/check_field'
  defaults:
    _controller: 'Drupal\route_with_custom_access\Controller\RouteWithCustomAccessController::returnOk'
  requirements:
    _field_value_parameter: 'node'
    _field_value_field: 'title'
    _field_value_value: 'Invalid title'
    _field_value_negate: 'TRUE'

This route uses our new access checker and is configured to check the value of the title field of the node we’re using.

The _field_value_parameter requirement tells the access checker that we want to look at the node entity in the node parameter.

The _field_value_field requirement indicates that we want to evaluate the title field, and the field_value_value requirement tells the access checker that it’s looking for the field value ‘Invalid title’.

Finally, the _field_value_negate requirement is true, stating that we want to allow access to nodes with titles not equal to “Invalid title”.

Now let’s create our controller. Create the web/modules/custom/route_with_custom_access/src/Controller directory and create a class RouteWithCustomAccessController.php that extends ControllerBase.

Give it a function so our controller can return a result.

<?php

namespace Drupal\route_with_custom_access\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\node\NodeInterface;

class RouteWithCustomAccessController extends ControllerBase {

  /**
  * Returns the word "Okay".
  *
  * @param \Drupal\node\NodeInterface $node
  * The node.
  *
  * @return string[]
  * The build array.
  */
  public function returnOk(NodeInterface $node) {
    return [
      '#markup' => 'Okay',
    ];
  }

}

All the controller method does is say “Okay” when we hit it. This will show us whether the access checker is working.

Reset the cache on your site. Now, let’s create a couple of nodes to test out our access checker.

Create an article node with a title of “Some title” and a body of “Some body.” For this post, we’ll assume that this is a node with a node ID of 1.

Create an article node with a title of “Invalid title” and a body of “Some body.” For this post, we’ll assume that this is a node with a node ID of 2.

Now browse to /node/1/check_field. Since the node we created has a title value other than “Invalid title” the access checker will allow access to it.

When we browse to /node/2/check_field, it will show us an Access denied page, since it evaluates the title field to be “Invalid title” and the check is negated, meaning a match of the field value will result in a forbidden route.

Now we have a functional access checker service that will allow or forbid access based on the value of a field of an entity parameter!


A full copy of the site and all of the custom code we’ve written is available here: https://github.com/jonathanfranks/access_check_field_value


71 views0 comments

Recent Posts

See All