Drupal 8: Poking A Hole In The Shield Module

The Shield module prevents access to a Drupal site by putting a Apache authentication system across the entire website. This means that to access the site you need a username and password.

This is useful in a number of different situations, but I use it most for protecting dev and staging sites from access. It's not the most complex authentication system in the world, but it's enough to prevent the embarrassment caused by having staging sites being spidered by search engines.

One problem it does introduce is when developing API endpoints. Because the Shield module prevents access to the entire site this means that the only way to test API endpoints is to turn off the module, exposing the whole site. Even if we leave Shield on there is a problem in that API endpoints usually have their own authentication systems they can often get confused between the shield authentication and their own authentication, which can cause a few headaches.

Thankfully, Drupal 8 allows us to override this functionality and poke holes in the Shield module to allow certain endpoints to be exposed without authentication. What we need to do is override the service definition for the Shield module and introduce our own class. Our custom class will check the incoming request and check it against certain conditions. If the conditions are met then we allow the request to occur normally. If they do not pass then we send the request onto the Shield module and the request goes through the authentication system.

I should point out that disabling the shield authentication leaves the routes open to access. As a result you should either be prepared for these endpoints being accessed anonymously or have another authentication system in place.

When loading the services on a site Drupal will automatically load a service provider class that can be used to alter the services. When the cache is cleared these classes can be used to alter the services in some way. These classes will only be loaded if they fulfil certain requirements. These requirements are as follows:

  • The machine name of the module is translated to remove all spaces and capitalise the first letter of every word.
  • The class name is kept in the /src directory of the module.
  • The class name must be suffixed by ServiceProvider.
  • The class must extend ServiceProviderBase.

In order to facilitate this we need to create a module, for this example we'll call it "Shield Hole" and give it the machine name of "shield_hole". Our class therefore needs to be called ShieldHoleServiceProvider.

<?php

namespace Drupal\shield_hole;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;

/**
 * Class ShieldHoleServiceProvider.
 *
 * @package Drupal\shield_hole
 */
class ShieldHoleServiceProvider extends ServiceProviderBase {

  /**
   * {@inheritdoc}
   */
  public function alter(ContainerBuilder $container) {
    // Decorate the shield module to prevent it from triggering on certain
    // routes.
    $definition = $container->getDefinition('shield.middleware');
    $definition->setClass('Drupal\shield_hole\ShieldOverride');
  }

}

When we clear the caches Drupal loads this class and runs the alter() method, passing in all of the defined services as the $container paramter. In this case we find the definition of the shield.midderware class and swaps it out for our own ShieldOverride class.

The ShieldOverride class is pretty simple, but it only has one job to do. By extending the ShieldMiddleware class we can either use it or bypass it altogether. The handle() method is called when a page request comes in and so it has access to all of the meta data for that page request. We then detect the correct page that we need to disable the authentication for. If this is the case then we handle the request with a normal Drupal response (done via the HTTP kernel object).

<?php

namespace Drupal\shield_hole;

use Drupal\shield\ShieldMiddleware;
use Symfony\Component\HttpFoundation\Request;

/**
 * Class ShieldOverride.
 *
 * @package Drupal\shield_hole
 */
class ShieldOverride extends ShieldMiddleware {

  /**
   * {@inheritdoc}
   */
  public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
    // Get the current request URI.
    $currentPath = $request->getRequestUri();

    // Get the current method (e.g. GET or POST).
    $currentMethod = $request->getMethod();

    if (($currentMethod == 'POST' || $currentMethod == 'GET') && strstr($currentPath, '/services/some_api') !== FALSE) {
      // If we are attempting to access the service then we handle the 
      // request without invoking the Shield module.
      return $this->httpKernel->handle($request, $type, $catch);
    }

    // Always handle the request using the default Shield behaviour.
    return parent::handle($request, $type, $catch);
  }

}

The default behaviour is to handle the request using the parent objects handle() method. As we have extended the ShieldMiddleware class the parent handle() method is essentially the authentication system. This means that if the request doesn't meet our requirements then it will be handled by the shield module at it should be and require the user to enter a password.

This example of using a ServiceProvider class to alter an existing service has many uses in Drupal 8. The Shield module override is a good example though as it shows how to alter a simple service to provide a known outcome. You just need to be aware of the security implications of doing this.

I have used ServiceProvider classes a few times to overload existing classes in contributed modules when I have needed to alter the way in which a module functions. This is especially useful when performing tests. If you are attempting to run some tests, especially against an API, then using a ServiceProvider to mock the service is a great way of providing your application with a known dataset without actually using the API.

For more information about this subject you can see the Drupal 8 service provider documentation.

Comments

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
15 + 1 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.