Skip to content

use tag for user context hash request invalidation #486

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Changelog
=========

2.6.0
-----

### Changed

* User context lookup now tags the hash lookup response. The logout listener can now invalidate that tag instead of
doing a BAN request. The previous varnish BAN request has been incorrect and banned all cache entries on Varnish.
The logout handler is now also activated by default for the Symfony HttpCache in addition to Varnish and Noop.

2.5.1
-----

Expand Down
5 changes: 3 additions & 2 deletions Resources/doc/reference/configuration/user-context.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,12 @@ or you will end up with mixed up caches.
~~~~~~~~~~~~~~~~~~

The logout handler will invalidate any cached user hashes when the user logs
out.
out. This will make sure that the session cookie of the logged out session can
not be abused to see protected cached content.

For the handler to work:

* your caching proxy should be :ref:`configured for BANs <foshttpcache:proxy-configuration>`
* your caching proxy must be :ref:`configured for tag invalidation <foshttpcache:proxy-configuration>`
* Symfony’s default behavior of regenerating the session id when users log in
and out must be enabled (``invalidate_session``).

Expand Down
9 changes: 7 additions & 2 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,17 @@ function ($v) {
return $v;
}

if (isset($v['proxy_client']['default']) && in_array($v['proxy_client']['default'], ['varnish', 'noop'])) {
if (isset($v['proxy_client']['default'])
&& in_array($v['proxy_client']['default'], ['varnish', 'symfony', 'noop'])
) {
$v['user_context']['logout_handler']['enabled'] = true;

return $v;
}
if (isset($v['proxy_client']['varnish']) || isset($v['proxy_client']['noop'])) {
if (isset($v['proxy_client']['varnish'])
|| isset($v['proxy_client']['symfony'])
|| isset($v['proxy_client']['noop'])
) {
$v['user_context']['logout_handler']['enabled'] = true;

return $v;
Expand Down
4 changes: 0 additions & 4 deletions src/DependencyInjection/FOSHttpCacheExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -313,10 +313,6 @@ private function loadUserContext(ContainerBuilder $container, XmlFileLoader $loa
->replaceArgument(0, $options);

if ($config['logout_handler']['enabled']) {
$container->getDefinition('fos_http_cache.user_context_invalidator')
->replaceArgument(1, $completeUserIdentifierHeaders)
->replaceArgument(2, $config['match']['accept']);

$container->setAlias('security.logout.handler.session', 'fos_http_cache.user_context.session_logout_handler');
} else {
$container->removeDefinition('fos_http_cache.user_context.logout_handler');
Expand Down
29 changes: 19 additions & 10 deletions src/EventListener/UserContextListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

namespace FOS\HttpCacheBundle\EventListener;

use FOS\HttpCache\ResponseTagger;
use FOS\HttpCache\UserContext\HashGenerator;
use FOS\HttpCacheBundle\UserContextInvalidator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
Expand Down Expand Up @@ -45,6 +47,13 @@ class UserContextListener implements EventSubscriberInterface
*/
private $hashGenerator;

/**
* If the response tagger is set, the hash lookup response is tagged with the session id for later invalidation.
*
* @var ResponseTagger|null
*/
private $responseTagger;

/**
* @var array
*/
Expand All @@ -60,26 +69,20 @@ class UserContextListener implements EventSubscriberInterface
* It prevents issues when the hash generator that is used returns a customized value for anonymous users,
* that differs from the documented, hardcoded one.
*
* @var RequestMatcherInterface
* @var RequestMatcherInterface|null
*/
private $anonymousRequestMatcher;

/**
* Constructor.
*
* @param RequestMatcherInterface $requestMatcher
* @param HashGenerator $hashGenerator
* @param RequestMatcherInterface|null $anonymousRequestMatcher
* @param array $options
*/
public function __construct(
RequestMatcherInterface $requestMatcher,
HashGenerator $hashGenerator,
RequestMatcherInterface $anonymousRequestMatcher = null,
ResponseTagger $responseTagger = null,
array $options = []
) {
$this->requestMatcher = $requestMatcher;
$this->hashGenerator = $hashGenerator;
$this->responseTagger = $responseTagger;
$this->anonymousRequestMatcher = $anonymousRequestMatcher;

$resolver = new OptionsResolver();
Expand Down Expand Up @@ -115,7 +118,8 @@ public function onKernelRequest(GetResponseEvent $event)
return;
}

if (!$this->requestMatcher->matches($event->getRequest())) {
$request = $event->getRequest();
if (!$this->requestMatcher->matches($request)) {
if ($event->getRequest()->headers->has($this->options['user_hash_header'])
&& !$this->isAnonymous($event->getRequest())
) {
Expand All @@ -127,6 +131,11 @@ public function onKernelRequest(GetResponseEvent $event)

$hash = $this->hashGenerator->generateHash();

if ($this->responseTagger && $request->hasSession()) {
$tag = UserContextInvalidator::buildTag($request->getSession()->getId());
$this->responseTagger->addTags([$tag]);
}

// status needs to be 200 as otherwise varnish will not cache the response.
$response = new Response('', 200, [
$this->options['user_hash_header'] => $hash,
Expand Down
3 changes: 1 addition & 2 deletions src/Resources/config/user_context.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<argument type="service" id="fos_http_cache.user_context.request_matcher" />
<argument type="service" id="fos_http_cache.user_context.hash_generator" />
<argument type="service" id="fos_http_cache.user_context.anonymous_request_matcher" />
<argument type="service" id="fos_http_cache.http.symfony_response_tagger" on-invalid="ignore" />
<argument>%fos_http_cache.event_listener.user_context.options%</argument>
<tag name="kernel.event_subscriber" />
</service>
Expand All @@ -27,8 +28,6 @@

<service id="fos_http_cache.user_context_invalidator" class="FOS\HttpCacheBundle\UserContextInvalidator">
<argument type="service" id="fos_http_cache.default_proxy_client" />
<argument />
<argument />
</service>

<service id="fos_http_cache.user_context.logout_handler" class="FOS\HttpCacheBundle\Security\Http\Logout\ContextInvalidationLogoutHandler" public="false">
Expand Down
40 changes: 12 additions & 28 deletions src/UserContextInvalidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,20 @@

namespace FOS\HttpCacheBundle;

use FOS\HttpCache\ProxyClient\Invalidation\BanCapable;
use FOS\HttpCache\ProxyClient\Invalidation\TagCapable;

class UserContextInvalidator
{
/**
* Service used to ban hash request.
*
* @var BanCapable
*/
private $banner;
const USER_CONTEXT_TAG_PREFIX = 'fos_http_cache_hashlookup-';

/**
* Accept header.
*
* @var string
* @var TagCapable
*/
private $acceptHeader;
private $tagger;

/**
* User identifier headers.
*
* @var string[]
*/
private $userIdentifierHeaders;

public function __construct(BanCapable $banner, $userIdentifierHeaders, $acceptHeader)
public function __construct(TagCapable $tagger)
{
$this->banner = $banner;
$this->acceptHeader = $acceptHeader;
$this->userIdentifierHeaders = $userIdentifierHeaders;
$this->tagger = $tagger;
}

/**
Expand All @@ -50,11 +34,11 @@ public function __construct(BanCapable $banner, $userIdentifierHeaders, $acceptH
*/
public function invalidateContext($sessionId)
{
foreach ($this->userIdentifierHeaders as $header) {
$this->banner->ban([
'accept' => $this->acceptHeader,
$header => sprintf('.*%s.*', $sessionId),
]);
}
$this->tagger->invalidateTags([static::buildTag($sessionId)]);
}

public static function buildTag($hash)
{
return static::USER_CONTEXT_TAG_PREFIX.$hash;
}
}
5 changes: 5 additions & 0 deletions tests/Functional/EventListener/UserContextListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace FOS\HttpCacheBundle\Tests\Functional\EventListener;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Session\SessionInterface;

class UserContextListenerTest extends WebTestCase
{
Expand All @@ -21,6 +22,9 @@ public function testHashLookup()
'PHP_AUTH_USER' => 'user',
'PHP_AUTH_PW' => 'user',
]);
/** @var SessionInterface $session */
$session = $client->getContainer()->get('session');
$session->setId('test');

$client->request('GET', '/secured_area/_fos_user_context_hash', [], [], [
'HTTP_ACCEPT' => 'application/vnd.fos.user-context-hash',
Expand All @@ -29,6 +33,7 @@ public function testHashLookup()

$this->assertTrue($response->headers->has('X-User-Context-Hash'), 'X-User-Context-Hash header missing on the response');
$this->assertEquals('5224d8f5b85429624e2160e538a3376a479ec87b89251b295c44ecbf7498ea3c', $response->headers->get('X-User-Context-Hash'), 'Not the expected context hash');
$this->assertEquals('fos_http_cache_hashlookup-test', $response->headers->get('X-Cache-Tags'));
$this->assertEquals('max-age=60, public', $response->headers->get('Cache-Control'));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,30 +32,20 @@ public function testLogout()
$session->save();

$mock = \Mockery::mock(Varnish::class);
$mock->shouldReceive('ban')
$mock->shouldReceive('invalidateTags')
->once()
->with([
'accept' => 'application/vnd.fos.user-context-hash',
'cookie' => '.*test.*',
])
;
$mock->shouldReceive('ban')
->once()
->with([
'accept' => 'application/vnd.fos.user-context-hash',
'authorization' => '.*test.*',
])
->with(['fos_http_cache_hashlookup-test'])
;
$mock->shouldReceive('flush')
->once()
->andReturn(2)
->andReturn(1)
;

$client->getContainer()->set('fos_http_cache.proxy_client.varnish', $mock);

$client->getCookieJar()->set(new Cookie($session->getName(), 'test'));
$client->request('GET', '/secured_area/logout');

$this->assertEquals(302, $client->getResponse()->getStatusCode(), $client->getResponse()->getContent());
$this->assertEquals(302, $client->getResponse()->getStatusCode(), substr($client->getResponse()->getContent(), 0, 2000));
}
}
2 changes: 1 addition & 1 deletion tests/Unit/DependencyInjection/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ public function testSupportsSymfony()
$expectedConfiguration['cache_manager']['generate_url_type'] = 'auto';
$expectedConfiguration['tags']['enabled'] = 'auto';
$expectedConfiguration['invalidation']['enabled'] = 'auto';
$expectedConfiguration['user_context']['logout_handler']['enabled'] = false;
$expectedConfiguration['user_context']['logout_handler']['enabled'] = true;

$formats = array_map(function ($path) {
return __DIR__.'/../../Resources/Fixtures/'.$path;
Expand Down
Loading