diff --git a/src/AccessControl/RouteAccessControlFactory.php b/src/AccessControl/RouteAccessControlFactory.php index 10897df..3d3d686 100644 --- a/src/AccessControl/RouteAccessControlFactory.php +++ b/src/AccessControl/RouteAccessControlFactory.php @@ -19,11 +19,8 @@ public function __construct( public function createRouteAccessControlData(string $name, Route $route): RouteAccessControlData { - $controller = $route->getDefault('_controller'); - $expression = RouteAccessControlData::NOT_API_PLATFORM_ROUTE; - - if ($controller && $this->isControllerCorrespondingToApiPlatform($controller)) { + if ($this->isControllerCorrespondingToApiPlatform($route)) { $expression = $this->getAccessControlExpressionForApiPlatform($route); } @@ -38,11 +35,14 @@ public function createRouteAccessControlData(string $name, Route $route): RouteA return $accessControlRouteData; } - protected function isControllerCorrespondingToApiPlatform(string $controller): bool + protected function isControllerCorrespondingToApiPlatform(Route $route): bool { + $controller = $route->getDefault('_controller'); + $resourceClass = $route->getDefault('_api_resource_class'); + $apiPlatformPrefix = 'api_platform'; - return 0 === mb_strpos($controller, $apiPlatformPrefix); + return 0 === mb_strpos($controller, $apiPlatformPrefix) || null !== $resourceClass; } protected function getAccessControlExpressionForApiPlatform(Route $route): string @@ -57,7 +57,7 @@ protected function getAccessControlExpressionForApiPlatform(Route $route): strin $resourceMetadata = $this->resourceMetadataCollectionFactory->create($resourceClass); $operation = $resourceMetadata->getOperation($operationName); - $isGranted = $operation->getSecurity(); + $isGranted = $operation->getSecurity() ?? $operation->getSecurityPostDenormalize(); if (null === $isGranted) { $isGranted = RouteAccessControlData::NO_ACCESS_CONTROL; } diff --git a/src/Command/AccessControlCheckerCommand.php b/src/Command/AccessControlCheckerCommand.php index 68e8ca4..d6b84bd 100644 --- a/src/Command/AccessControlCheckerCommand.php +++ b/src/Command/AccessControlCheckerCommand.php @@ -4,13 +4,13 @@ namespace Theodo\AccentBundle\Command; -use Theodo\AccentBundle\AccessControl\AccentReportFactory; -use Theodo\AccentBundle\Descriptor\AccessControlDescriptor; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Theodo\AccentBundle\AccessControl\AccentReportFactory; +use Theodo\AccentBundle\Descriptor\AccessControlDescriptor; #[AsCommand('theodo:access-control', 'Check access control for each route.')] class AccessControlCheckerCommand extends Command @@ -54,7 +54,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln($accentReport->getUnprotectedRoutesCount().' unprotected route(s)'); - $exitCode = (0 < $accentReport->getUnprotectedRoutesCount()) ? 1 : 0; + $exitCode = (0 < $accentReport->getUnprotectedRoutesCount()) ? Command::FAILURE : Command::SUCCESS; return $exitCode; } diff --git a/src/Descriptor/AccessControlDescriptor.php b/src/Descriptor/AccessControlDescriptor.php index 3a4b4db..15c32cc 100644 --- a/src/Descriptor/AccessControlDescriptor.php +++ b/src/Descriptor/AccessControlDescriptor.php @@ -4,9 +4,9 @@ namespace Theodo\AccentBundle\Descriptor; -use Theodo\AccentBundle\AccessControl\RouteAccessControlData; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Output\OutputInterface; +use Theodo\AccentBundle\AccessControl\RouteAccessControlData; class AccessControlDescriptor { @@ -14,8 +14,10 @@ class AccessControlDescriptor RouteAccessControlData::NO_ACCESS_CONTROL => 'No access control.', RouteAccessControlData::NOT_API_PLATFORM_ROUTE => 'This route is not linked to API Platform.', RouteAccessControlData::RESOURCE_NOT_FOUND => 'The resource linked to his route was not found.', + RouteAccessControlData::OPERATION_NOT_FOUND => 'This route does not seem to be linked to a valid operation.', RouteAccessControlData::RESOURCE_UNRELATED_ROUTE => 'This route is not linked to a resource', ]; + private $output; /** diff --git a/tests/BundleInitializationTest.php b/tests/BundleInitializationTest.php index a6c3aef..421ee01 100644 --- a/tests/BundleInitializationTest.php +++ b/tests/BundleInitializationTest.php @@ -2,58 +2,20 @@ declare(strict_types=1); -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Nyholm\BundleTest\TestKernel; use ApiPlatform\Symfony\Bundle\ApiPlatformBundle; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; +use Nyholm\BundleTest\TestKernel; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Console\Command\Command; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\KernelInterface; -use Theodo\AccentBundle\TheodoAccentBundle; use Theodo\AccentBundle\AccessControl\AccentReportFactory; use Theodo\AccentBundle\AccessControl\RouteAccessControlData; +use Theodo\AccentBundle\TheodoAccentBundle; class BundleInitializationTest extends KernelTestCase { - protected static function getKernelClass(): string - { - return TestKernel::class; - } - - protected static function createKernel(array $options = []): KernelInterface - { - /** - * @var TestKernel $kernel - */ - $kernel = parent::createKernel($options); - $kernel->addTestBundle(DoctrineBundle::class); - $kernel->addTestBundle(ApiPlatformBundle::class); - $kernel->addTestBundle(TheodoAccentBundle::class); - $kernel->addTestConfig(__DIR__.'/config/packages/doctrine.yaml'); - $kernel->addTestConfig(__DIR__.'/config/packages/api_platform.yaml'); - $kernel->addTestRoutingFile(__DIR__.'/config/routes.yaml'); - - // Unused, non public services are removed during kernel boot. - // We force them as public during test. - $kernel->addTestCompilerPass(new class () implements CompilerPassInterface { - public function process(ContainerBuilder $container): void - { - $services = $container->getDefinitions() + $container->getAliases(); - - foreach ($services as $id => $definition) { - if (stripos($id, 'theodo_accent') === 0) { - $definition->setPublic(true); - } - } - } - }); - - $kernel->handleOptions($options); - - return $kernel; - } - public function testInitBundle(): void { $container = self::getContainer(); @@ -80,9 +42,9 @@ public function testExposedRoutes(): void fn ($route) => RouteAccessControlData::RESOURCE_UNRELATED_ROUTE !== $route->getExpression() ); - $this->assertCount(3, $resourceRelatedRoutes); + $this->assertCount(6, $resourceRelatedRoutes); $checkedRoutes = []; - foreach($resourceRelatedRoutes as $routeData) { + foreach ($resourceRelatedRoutes as $routeData) { $checkedRoutes[$routeData->getRouteName()] = $routeData->getExpression(); } @@ -90,8 +52,49 @@ public function testExposedRoutes(): void '_api_/books/{id}{._format}_get' => "is_granted('ROLE_USER_GET')", '_api_/books{._format}_post' => "is_granted('ROLE_USER_DEFAULT')", '_api_/books/{id}{._format}_patch' => "is_granted('ROLE_USER_PATCH')", + '_api_/authors/{id}{._format}_put' => "is_granted('ROLE_USER_PUT')", + '_api_/authors/{id}{._format}_get' => "is_granted('ROLE_USER_GET')", + 'book_get_publication' => "is_granted('ROLE_USER_CUSTOM_CONTROLLER')", ]; $this->assertEquals($expectedRoutes, $checkedRoutes); } + + protected static function getKernelClass(): string + { + return TestKernel::class; + } + + protected static function createKernel(array $options = []): KernelInterface + { + /** + * @var TestKernel $kernel + */ + $kernel = parent::createKernel($options); + $kernel->addTestBundle(DoctrineBundle::class); + $kernel->addTestBundle(ApiPlatformBundle::class); + $kernel->addTestBundle(TheodoAccentBundle::class); + $kernel->addTestConfig(__DIR__.'/config/packages/doctrine.yaml'); + $kernel->addTestConfig(__DIR__.'/config/packages/api_platform.yaml'); + $kernel->addTestRoutingFile(__DIR__.'/config/routes.yaml'); + + // Unused, non public services are removed during kernel boot. + // We force them as public during test. + $kernel->addTestCompilerPass(new class() implements CompilerPassInterface { + public function process(ContainerBuilder $container): void + { + $services = $container->getDefinitions() + $container->getAliases(); + + foreach ($services as $id => $definition) { + if (0 === mb_stripos($id, 'theodo_accent')) { + $definition->setPublic(true); + } + } + } + }); + + $kernel->handleOptions($options); + + return $kernel; + } } diff --git a/tests/Controller/BookController.php b/tests/Controller/BookController.php new file mode 100644 index 0000000..161eef0 --- /dev/null +++ b/tests/Controller/BookController.php @@ -0,0 +1,18 @@ +