In the Symfony2 security documentation both the firewalls and the access control rules are demonstrated using the "path" option, which is used to determine if a firewall or rule is applicable to the current URL. Also the "ip" option is demonstrated. The fact of the matter is, the string based configuration options in security.yml are transformed into objects of class RequestMatcher. This is a curious class in the HttpFoundation component which allows you to match a given Request object. The Security component uses it to determine if it should activate a certain firewall for the current request (usually only by checking the request's path info).

In the Security Component's reference an extra option is mentioned (only for firewalls), called "request_matcher". This suggests we can create our own request matcher. And so we can!

Creating an advanced request matcher

A request matcher should implement RequestMatcherInterface, which prescribes just one method, match(). It receives as its only argument the current Request object.

This means, we can match the current request using any information stored in the Request object, like the session, cookies, query parameters, request parameters and server parameters. Lots of opportunities!

For instance, if we would like to activate a firewall only when a certain HTTP header was sent (in the example below: the "X-WSSE" header), we can use the following request matcher:

namespace Matthias\SecurityBundle\Security;

use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\HttpFoundation\Request;

class WsseHeaderRequestMatcher implements RequestMatcherInterface
{
    public function matches(Request $request)
    {
        return $request->headers->has('x-wsse');
    }
}

Hook the request matcher in the security configuration

To be able to use the request matcher in the firewall configuration, we have to create a service for it:

<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>
        <service id="matthias_security.wsse_request_matcher"
                 class="Matthias\SecurityBundle\Security\WsseHeaderRequestMatcher">
        </service>
    </services>
</container>

And finally we should add it to the appropriate firewall in security.yml:

security:
    firewalls:
        secured_area:
            request_matcher: matthias_security.wsse_request_matcher

            # we don't use pattern anymore
            #pattern: ^/demo/secured

Configuring access control rules with a custom RequestMatcher

Using a custom request matcher for the access control rules, is a bit more tricky. You need to create a compiler pass for the service container, to modify the security.access_map service definition. The goal is to call the method add() on the AccessMap class, to register the request matcher, its corresponding roles and optionally a required channel (http or https).

First, create the compiler pass:

namespace Matthias\SecurityBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class AccessControlPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('security.access_map')) {
            return;
        }

        $requiresChannel = 'https';

        $accessMapDefinition = $container->getDefinition('security.access_map');
        $accessMapDefinition
            ->addMethodCall(
                'add', array(
                    new Reference('matthias_security.wsse_request_matcher'), 
                    array('ROLE_WEBSERVICE'), 
                    $requiresChannel),
                );
    }
}

Register the compiler pass in the build() method of your bundle:

namespace Matthias\SecurityBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Matthias\SecurityBundle\DependencyInjection\Compiler\AccessControlPass;

class MatthiasSecurityBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container->addCompilerPass(new AccessControlPass());
    }
}

This is about the easiest it can get. I think a more elegant solution would not be far away, so I might do a pull request some day.

Using request matchers with Silex

When you have registered the SecurityServiceProvider with Silex, you can use a custom request matcher when defining access control rules like this:

use Matthias\SecurityBundle\Security\WsseRequestMatcher;

// ...

$app['wsse_request_matcher'] = new WsseRequestMatcher();

$app['security.access_rules'] = array(
    array($app['wsse_request_matcher'], 'ROLE_WEBSERVICE'),
);

Using the custom request matcher to activate firewalls (like demonstrated above for the Symfony2 case) works like this:

$app['security.firewalls'] = array(
    'secured' => array(
        'pattern' => $app['wsse_request_matcher'],
        // ...
    ),
);
PHP Security Symfony2 compiler pass firewall request service container
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
jafar

From what i've read, the same could be achieved for access control rules, using firewall listeners.However I couldn't find out how set up the listener and hook to the firewall.or maybe I'm wrong in my initial assumption.

Guest

This has been a lifesaver.
Thanks a lot.
Just one question.I'm trying to use the second part, which is customizing the access control rules.how can i use the default "path" RequestMatcher?

Edit:
Thought I found the answer.I was wrong.
Thanks again.

John White

Awesome, thanks for this!

I needed to implement security on all HTTP methods other than OPTIONS with silex and was able to use a HTTPMethodsRequestMatcher which used a whitelist.

Thanks!

Petter

What do you think is a better solution?... Implement this way or defining a filter to ask if user is allowed to access to that URI? Both cases loading from the DB. In my opinnion the way you explain is a clearer and faster way, but what do you think?

Loïc Favory

Interesting !
Thanks for sharing it.

Neal

That's amazing, thanks so much. That means I have to actually do some wotk now! :D

Matthias Noback

You're welcome!

Neal

"Due to a shortcoming in the way the Security Bundle configures the access control rules, you can not (yet) use a custom request matcher for those." - Dammit, I found your blog post by googling exactly for that. I was all-ready to start writing my own service before I noticed that crushing line in your article. How would you go about modifying Symfony to allow the access control rules to be loaded by a service?

Use case: I want to give my admin users the ability to deny / allow non admin user groups access to certain pages in the app (loaded from the db), without modifying security.yml.

Matthias Noback

Hi Neal, an answer to your question was found faster than I thought - I have edited the article to contain the solution to your problem. Good luck!

Matthias Noback

Hi, I was a bit disappointed about this too... I will take a look at the configuration and answer your question later (just got back from my vacation ;)).

Matthias Noback

Thanks for reading ;)

Tobias Sjösten

Good stuff! Thanks for sharing.