Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 0 additions & 4 deletions src/bundle/Controller/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,4 @@

abstract class Controller extends AbstractController
{
public function performAccessCheck(): void
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
}
}
6 changes: 5 additions & 1 deletion src/bundle/Controller/PasswordChangeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

use Exception;
use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Contracts\User\Controller\AuthenticatedRememberedCheckTrait;
use Ibexa\Contracts\User\Controller\RestrictedControllerInterface;
use Ibexa\Core\MVC\Symfony\SiteAccess;
use Ibexa\User\ExceptionHandler\ActionResultHandler;
use Ibexa\User\Form\Factory\FormFactory;
Expand All @@ -20,8 +22,10 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class PasswordChangeController extends Controller
class PasswordChangeController extends Controller implements RestrictedControllerInterface
{
use AuthenticatedRememberedCheckTrait;

private ActionResultHandler $actionResultHandler;

private UserService $userService;
Expand Down
6 changes: 5 additions & 1 deletion src/bundle/Controller/UserInvitationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

namespace Ibexa\Bundle\User\Controller;

use Ibexa\Contracts\User\Controller\AuthenticatedRememberedCheckTrait;
use Ibexa\Contracts\User\Controller\RestrictedControllerInterface;
use Ibexa\Contracts\User\Invitation\Exception\InvitationAlreadyExistsException;
use Ibexa\Contracts\User\Invitation\Exception\UserAlreadyExistsException;
use Ibexa\Contracts\User\Invitation\InvitationCreateStruct;
Expand All @@ -20,8 +22,10 @@
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;

final class UserInvitationController extends Controller
final class UserInvitationController extends Controller implements RestrictedControllerInterface
{
use AuthenticatedRememberedCheckTrait;

private InvitationService $invitationService;

private InvitationSender $mailSender;
Expand Down
6 changes: 5 additions & 1 deletion src/bundle/Controller/UserSettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
namespace Ibexa\Bundle\User\Controller;

use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\User\Controller\AuthenticatedRememberedCheckTrait;
use Ibexa\Contracts\User\Controller\RestrictedControllerInterface;
use Ibexa\User\ExceptionHandler\ActionResultHandler;
use Ibexa\User\Form\Data\UserSettingUpdateData;
use Ibexa\User\Form\Factory\FormFactory;
Expand All @@ -24,8 +26,10 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class UserSettingsController extends Controller
class UserSettingsController extends Controller implements RestrictedControllerInterface
{
use AuthenticatedRememberedCheckTrait;

private FormFactory $formFactory;

private SubmitHandler $submitHandler;
Expand Down
1 change: 0 additions & 1 deletion src/bundle/Resources/config/services/controllers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ services:
Ibexa\Bundle\User\Controller\Controller:
calls:
- [setContainer , ['@Psr\Container\ContainerInterface']]
- [performAccessCheck, []]

Ibexa\Bundle\User\Controller\PasswordResetController:
calls:
Expand Down
17 changes: 17 additions & 0 deletions src/contracts/Controller/AuthenticatedRememberedCheckTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Contracts\User\Controller;

trait AuthenticatedRememberedCheckTrait
{
public function performAccessCheck(): void
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
}
}
14 changes: 14 additions & 0 deletions src/contracts/Controller/RestrictedControllerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Contracts\User\Controller;

interface RestrictedControllerInterface
{
public function performAccessCheck(): void;
}
60 changes: 60 additions & 0 deletions src/lib/EventListener/PerformAccessCheckSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\User\EventListener;

use Ibexa\Contracts\User\Controller\RestrictedControllerInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;

final class PerformAccessCheckSubscriber implements EventSubscriberInterface
{
/**
* @param iterable<object> $controllers
*/
public function __construct(
#[AutowireIterator('controller.service_arguments')]
private readonly iterable $controllers
) {
}

public function onControllerEvent(ControllerEvent $event): void
{
$controller = $event->getController();
if (is_array($controller) && $controller[0] instanceof RestrictedControllerInterface) {
$controller[0]->performAccessCheck();

return;
}

if ($controller instanceof RestrictedControllerInterface) {
$controller->performAccessCheck();

return;
}

if (is_string($controller) && str_contains($controller, '::')) {
[$class] = explode('::', $controller, 2);

foreach ($this->controllers as $controllerInstance) {
if ($controllerInstance::class === $class && $controllerInstance instanceof RestrictedControllerInterface) {
$controllerInstance->performAccessCheck();
break;
}
}
}
}

public static function getSubscribedEvents(): array
{
return [
ControllerEvent::class => 'onControllerEvent',
];
}
}
106 changes: 106 additions & 0 deletions tests/lib/EventListener/PerformAccessCheckSubscriberTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\User\EventListener;

use Ibexa\Contracts\User\Controller\RestrictedControllerInterface;
use Ibexa\User\EventListener\PerformAccessCheckSubscriber;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;

final class PerformAccessCheckSubscriberTest extends TestCase
{
private PerformAccessCheckSubscriber $subscriber;

private HttpKernelInterface $kernel;

private Request $request;

protected function setUp(): void
{
$this->kernel = $this->createMock(HttpKernelInterface::class);
$this->request = new Request();
$this->subscriber = new PerformAccessCheckSubscriber([]);
}

public function testArrayController(): void
{
$controller = new MockControllerInterface();
$event = new ControllerEvent(
$this->kernel,
[$controller, 'action'],
$this->request,
HttpKernelInterface::MAIN_REQUEST
);

$this->subscriber->onControllerEvent($event);

self::assertTrue($controller->wasCheckPerformed());
}

public function testInvokableController(): void
{
$controller = new MockControllerInterface();
$event = new ControllerEvent(
$this->kernel,
$controller,
$this->request,
HttpKernelInterface::MAIN_REQUEST
);

$this->subscriber->onControllerEvent($event);

self::assertTrue($controller->wasCheckPerformed());
}

public function testStringControllerWithServiceLookup(): void
{
$controller = new MockControllerInterface();
$this->subscriber = new PerformAccessCheckSubscriber([$controller]);

$event = new ControllerEvent(
$this->kernel,
MockControllerInterface::class . '::staticAction',
$this->request,
HttpKernelInterface::MAIN_REQUEST
);

$this->subscriber->onControllerEvent($event);

self::assertTrue($controller->wasCheckPerformed());
}
}

final class MockControllerInterface implements RestrictedControllerInterface
{
private bool $checkPerformed = false;

public function performAccessCheck(): void
{
$this->checkPerformed = true;
}

public function wasCheckPerformed(): bool
{
return $this->checkPerformed;
}

public function __invoke(): void
{
}

public function action(): void
{
}

public static function staticAction(): void
{
}
}
Loading