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="https://symfony.com/schema/dic/services"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://symfony.com/schema/dic/services https://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'],
// ...
),
);
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.
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.
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!
Great!
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?
Interesting !
Thanks for sharing it.
That's amazing, thanks so much. That means I have to actually do some wotk now! :D
You're welcome!
"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.
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!
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 ;)).
Thanks for reading ;)
Good stuff! Thanks for sharing.