8000 [Security] Deprecate callable firewall listeners by MatTheCat · Pull Request #60614 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Security] Deprecate callable firewall listeners #60614

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
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
7 changes: 7 additions & 0 deletions UPGRADE-7.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ HttpFoundation
--------------

* Deprecate using `Request::sendHeaders()` after headers have already been sent; use a `StreamedResponse` instead

Security
--------

* Deprecate callable firewall listeners, extend `AbstractListener` or implement `FirewallListenerInterface` instead
* Deprecate `AbstractListener::__invoke`
* Deprecate `LazyFirewallContext::__invoke()`
1 change: 1 addition & 0 deletions src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ CHANGELOG
) {
}
```
* Deprecate `LazyFirewallContext::__invoke()`

7.3
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Http\Authenticator\Debug\TraceableAuthenticatorManagerListener;
use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Contracts\Service\ResetInterface;

Expand Down Expand Up @@ -88,7 +89,11 @@ protected function callListeners(RequestEvent $event, iterable $listeners): void
}

foreach ($requestListeners as $listener) {
$listener($event);
if (!$listener instanceof FirewallListenerInterface) {
$listener($event);
} 8000 elseif (false !== $listener->supports($event->getRequest())) {
$listener->authenticate($event);
}

if ($event->hasResponse()) {
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Bundle\SecurityBundle\Security;

use Symfony\Component\Security\Http\Firewall\ExceptionListener;
use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\Firewall\LogoutListener;

/**
Expand Down Expand Up @@ -39,7 +40,7 @@ public function getConfig(): ?FirewallConfig
}

/**
* @return iterable<mixed, callable>
* @return iterable<mixed, FirewallListenerInterface|callable>
*/
public function getListeners(): iterable
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@

namespace Symfony\Bundle\SecurityBundle\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Http\Event\LazyResponseEvent;
use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\Firewall\LogoutListener;
Expand All @@ -23,7 +25,7 @@
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class LazyFirewallContext extends FirewallContext
class LazyFirewallContext extends FirewallContext implements FirewallListenerInterface
{
public function __construct(
iterable $listeners,
Expand All @@ -40,19 +42,26 @@ public function getListeners(): iterable
return [$this];
}

public function __invoke(RequestEvent $event): void
public function supports(Request $request): ?bool
{
return true;
}

public function authenticate(RequestEvent $event): void
{
$listeners = [];
$request = $event->getRequest();
$lazy = $request->isMethodCacheable();

foreach (parent::getListeners() as $listener) {
if (!$lazy || !$listener instanceof FirewallListenerInterface) {
if (!$listener instanceof FirewallListenerInterface) {
trigger_deprecation('symfony/security-http', '7.4', 'Using a callable as firewall listener is deprecated, extend "%s" or implement "%s" instead.', AbstractListener::class, FirewallListenerInterface::class);

$listeners[] = $listener;
$lazy = $lazy && $listener instanceof FirewallListenerInterface;
$lazy = false;
} elseif (false !== $supports = $listener->supports($request)) {
$listeners[] = [$listener, 'authenticate'];
$lazy = null === $supports;
$lazy = $lazy && null === $supports;
}
}

Expand All @@ -75,4 +84,19 @@ public function __invoke(RequestEvent $event): void
}
});
}

public static function getPriority(): int
{
return 0;
}

/**
* @deprecated since Symfony 7.4, to be removed in 8.0
*/
public function __invoke(RequestEvent $event): void
{
trigger_deprecation('symfony/security-bundle', '7.4', 'The "%s()" method is deprecated since Symfony 7.4 and will be removed in 8.0.', __METHOD__);

$this->authenticate($event);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Role\RoleHierarchy;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\FirewallMapInterface;
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
use Symfony\Component\VarDumper\Caster\ClassStub;
Expand Down Expand Up @@ -193,8 +195,18 @@ public function testGetListeners()
$request = new Request();
$event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
$event->setResponse($response = new Response());
$listener = function ($e) use ($event, &$listenerCalled) {
$listenerCalled += $e === $event;
$listener = new class extends AbstractListener {
public int $callCount = 0;

public function supports(Request $request): ?bool
{
return true;
}

public function authenticate(RequestEvent $event): void
{
++$this->callCount;
}
};
$firewallMap = $this
->getMockBuilder(FirewallMap::class)
Expand All @@ -217,9 +229,9 @@ public function testGetListeners()
$collector = new SecurityDataCollector(null, null, null, null, $firewallMap, $firewall, true);
$collector->collect($request, $response);

$this->assertNotEmpty($collected = $collector->getListeners()[0]);
$this->assertCount(1, $collector->getListeners());
$collector->lateCollect();
$this->assertSame(1, $listenerCalled);
$this->assertSame(1, $listener->callCount);
}

public function testCollectCollectsDecisionLogWhenStrategyIsAffirmative()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener;
use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;

/**
Expand All @@ -41,9 +43,19 @@ public function testOnKernelRequestRecordsListeners()
{
$request = new Request();
$event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);
$event->setResponse($response = new Response());
$listener = function ($e) use ($event, &$listenerCalled) {
$listenerCalled += $e === $event;
$event->setResponse(new Response());
$listener = new class extends AbstractListener {
public int $callCount = 0;

public function supports(Request $request): ?bool
{
return true;
}

public function authenticate(RequestEvent $event): void
{
++$this->callCount;
}
};
$firewallMap = $this->createMock(FirewallMap::class);
$firewallMap
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Bundle/SecurityBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"symfony/clock": "^6.4|^7.0|^8.0",
"symfony/config": "^7.3|^8.0",
"symfony/dependency-injection": "^6.4.11|^7.1.4|^8.0",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/http-kernel": "^6.4|^7.0|^8.0",
"symfony/http-foundation": "^6.4|^7.0|^8.0",
Expand Down
6 changes: 6 additions & 0 deletions src/Symfony/Component/Security/Http/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
CHANGELOG
=========

7.4
---

* Deprecate callable firewall listeners, extend `AbstractListener` or implement `FirewallListenerInterface` instead
* Deprecate `AbstractListener::__invoke`

7.3
---

Expand Down
7 changes: 5 additions & 2 deletions src/Symfony/Component/Security/Http/Firewall.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
Expand Down Expand Up @@ -123,6 +124,8 @@ protected function callListeners(RequestEvent $event, iterable $listeners)
{
foreach ($listeners as $listener) {
if (!$listener instanceof FirewallListenerInterface) {
trigger_deprecation('symfony/security-http', '7.4', 'Using a callable as firewall listener is deprecated, extend "%s" or implement "%s" instead.', AbstractListener::class, FirewallListenerInterface::class);

$listener($event);
} elseif (false !== $listener->supports($event->getRequest())) {
$listener->authenticate($event);
Expand All @@ -134,8 +137,8 @@ protected function callListeners(RequestEvent $event, iterable $listeners)
}
}

private function getListenerPriority(object $logoutListener): int
private function getListenerPriority(object $listener): int
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remember to inline this method on 8.0

{
return $logoutListener instanceof FirewallListenerInterface ? $logoutListener->getPriority() : 0;
return $listener instanceof FirewallListenerInterface ? $listener->getPriority() : 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@
*/
abstract class AbstractListener implements FirewallListenerInterface
{
/**
* @deprecated since Symfony 7.4, to be removed in 8.0
*/
final public function __invoke(RequestEvent $event): void
{
trigger_deprecation('symfony/security-http', '7.4', 'The "%s()" method is deprecated since Symfony 7.4 and will be removed in 8.0.', __METHOD__);

if (false !== $this->supports($event->getRequest())) {
$this->authenticate($event);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ public function testHandleWhenTheAccessDecisionManagerDecidesToRefuseAccess()

$this->expectException(AccessDeniedException::class);

$listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
$this->assertTrue($listener->supports($request));
$listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}

public function testHandleWhenThereIsNoAccessMapEntryMatchingTheRequest()
Expand All @@ -95,7 +96,8 @@ public function testHandleWhenThereIsNoAccessMapEntryMatchingTheRequest()
$accessMap
);

$listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
$this->assertNull($listener->supports($request));
$listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}

public function testHandleWhenAccessMapReturnsEmptyAttributes()
Expand Down Expand Up @@ -124,7 +126,8 @@ public function testHandleWhenAccessMapReturnsEmptyAttributes()

$event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);

$listener(new LazyResponseEvent($event));
$this->assertNull($listener->supports($request));
$listener->authenticate(new LazyResponseEvent($event));
}

public function testHandleWhenTheSecurityTokenStorageHasNoToken()
Expand Down Expand Up @@ -154,7 +157,8 @@ public function testHandleWhenTheSecurityTokenStorageHasNoToken()

$this->expectException(AccessDeniedException::class);

$listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
$this->assertTrue($listener->supports($request));
$listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}

public function testHandleWhenPublicAccessIsAllowed()
Expand Down Expand Up @@ -182,7 +186,8 @@ public function testHandleWhenPublicAccessIsAllowed()
false
);

$listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
$this->assertNull($listener->supports($request));
$listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}

public function testHandleWhenPublicAccessWhileAuthenticated()
Expand Down Expand Up @@ -212,7 +217,8 @@ public function testHandleWhenPublicAccessWhileAuthenticated()
false
);

$listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
$this->assertNull($listener->supports($request));
$listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}

public function testHandleMWithultipleAttributesShouldBeHandledAsAnd()
Expand Down Expand Up @@ -246,7 +252,8 @@ public function testHandleMWithultipleAttributesShouldBeHandledAsAnd()
$accessMap
);

$listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
$this->assertTrue($listener->supports($request));
$listener->authenticate(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
}

public function testLazyPublicPagesShouldNotAccessTokenStorage()
Expand All @@ -263,7 +270,9 @@ public function testLazyPublicPagesShouldNotAccessTokenStorage()
;

$listener = new AccessListener($tokenStorage, $this->createMock(AccessDecisionManagerInterface::class), $accessMap, false);
$listener(new LazyResponseEvent(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)));

$this->assertNull($listener->supports($request));
$listener->authenticate(new LazyResponseEvent(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)));
}

public function testConstructWithTrueExceptionOnNoToken()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,8 @@ public function testHandleWithNotSecuredRequestAndHttpChannel()
->willReturn([[], 'http'])
;

$event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);

$listener = new ChannelListener($accessMap);
$listener($event);

$this->assertNull($event->getResponse());
$this->assertFalse($listener->supports($request));
}

public function testHandleWithSecuredRequestAndHttpsChannel()
Expand All @@ -64,12 +60,8 @@ public function testHandleWithSecuredRequestAndHttpsChannel()
->willReturn([[], 'https'])
;

$event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);

$listener = new ChannelListener($accessMap);
$listener($event);

$this->assertNull($event->getResponse());
$this->assertFalse($listener->supports($request));
}

public function testHandleWithNotSecuredRequestAndHttpsChannel()
Expand All @@ -92,7 +84,9 @@ public function testHandleWithNotSecuredRequestAndHttpsChannel()
$event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);

$listener = new ChannelListener($accessMap);
$listener($event);
$this->assertTrue($listener->supports($request));

$listener->authenticate($event);

$response = $event->getResponse();
$this->assertInstanceOf(RedirectResponse::class, $response);
Expand All @@ -119,7 +113,9 @@ public function testHandleWithSecuredRequestAndHttpChannel()
$event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST);

$listener = new ChannelListener($accessMap);
$listener($event);
$this->assertTrue($listener->supports($request));

$listener->authenticate($event);

$response = $event->getResponse();
$this->assertInstanceOf(RedirectResponse::class, $response);
Expand Down
Loading
Loading
0