diff --git a/src/EventSubscriber/SerializerSubscriber.php b/src/EventSubscriber/SerializerSubscriber.php index 404fbb8..9325cd4 100644 --- a/src/EventSubscriber/SerializerSubscriber.php +++ b/src/EventSubscriber/SerializerSubscriber.php @@ -12,7 +12,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\ControllerEvent; +use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ViewEvent; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -41,11 +41,11 @@ class SerializerSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents() { return [ - KernelEvents::CONTROLLER => [ - ['onKernelController', 10], + KernelEvents::CONTROLLER_ARGUMENTS => [ + ['onKernelControllerArguments', -10], ], KernelEvents::VIEW => [ - ['onKernelView', -1], + ['onKernelView', -10], ], KernelEvents::EXCEPTION => [ ['onKernelValidateException', 257], @@ -69,7 +69,7 @@ public function setValidator(ValidatorInterface $validator): self { return $this; } - public function onKernelController(ControllerEvent $event) { + public function onKernelControllerArguments(ControllerArgumentsEvent $event) { $request = $event->getRequest(); diff --git a/src/Request/ParamConverter/PostRestParamConverter.php b/src/Request/ParamConverter/PostRestParamConverter.php index ca3d7ef..f1a6e67 100644 --- a/src/Request/ParamConverter/PostRestParamConverter.php +++ b/src/Request/ParamConverter/PostRestParamConverter.php @@ -6,13 +6,24 @@ use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface; use GollumSF\RestBundle\Annotation\Unserialize; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; +use Symfony\Component\Serializer\SerializerInterface; class PostRestParamConverter implements ParamConverterInterface { /** @var DoctrineParamConverter */ private $doctrineParamConverter; + + /** @var SerializerInterface */ + private $serializer; + + public function __construct(SerializerInterface $serializer) { + $this->serializer = $serializer; + } - public function setDoctrineParamConverter(DoctrineParamConverter $doctrineParamConverter): void { + public function setDoctrineParamConverter(DoctrineParamConverter $doctrineParamConverter): void + { $this->doctrineParamConverter = $doctrineParamConverter; } @@ -20,6 +31,7 @@ public function apply(Request $request, ParamConverter $configuration) { /** @var Unserialize $unserializeAnnotation */ $unserializeAnnotation = $request->attributes->get('_'.Unserialize::ALIAS_NAME); $configurationName = $configuration->getName(); + $class = $configuration->getClass(); if ( $unserializeAnnotation && @@ -32,7 +44,22 @@ public function apply(Request $request, ParamConverter $configuration) { $this->doctrineParamConverter->apply($request, $configuration); $configuration->setIsOptional($isOptional); } - $request->attributes->set('_'.Unserialize::ALIAS_NAME.'_class', $configuration->getClass()); + if (!$request->attributes->get($configurationName)) { + $content = $request->getContent(); + if ($content) { + try { + $entity = $this->serializer->deserialize($content, $class, 'json', $context = [ + 'groups' => $unserializeAnnotation->getGroups(), + ]); + $request->attributes->set($configurationName, $entity); + } catch (MissingConstructorArgumentsException $e) { + throw new BadRequestHttpException($e->getMessage()); + } catch (\UnexpectedValueException $e) { + throw new BadRequestHttpException($e->getMessage()); + } + } + } + $request->attributes->set('_'.Unserialize::ALIAS_NAME.'_class', $class); return true; } return false; diff --git a/tests/Integration/Controller/Api/AbstractControllerTest.php b/tests/Integration/Controller/Api/AbstractControllerTest.php index 7be774b..259b3bf 100644 --- a/tests/Integration/Controller/Api/AbstractControllerTest.php +++ b/tests/Integration/Controller/Api/AbstractControllerTest.php @@ -44,6 +44,7 @@ protected function getKernel(): KernelInterface { $this->kernel->addBundle(\Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class); $this->kernel->addBundle(\Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class); $this->kernel->addBundle(\Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class); + $this->kernel->addBundle(\Symfony\Bundle\SecurityBundle\SecurityBundle::class); $this->kernel->addCompilerPasses([ new PublicServicePass('|GollumSF*|') ]); diff --git a/tests/Integration/Controller/Api/BookControllerTest.php b/tests/Integration/Controller/Api/BookControllerTest.php index eabde6b..0d80467 100644 --- a/tests/Integration/Controller/Api/BookControllerTest.php +++ b/tests/Integration/Controller/Api/BookControllerTest.php @@ -471,6 +471,25 @@ public function testPut($content) { $this->assertEquals($book->getTitle(), 'TITLE_NEW_1'); $this->assertEquals($book->getDescription(), 'DESCRIPTION_NEW_1'); } + + public function testPostIsGranted() { + $this->loadFixture(); + + /** @var ExceptionSubscriber $exceptionSubscriber */ + $exceptionSubscriber = $this->getContainer()->get(ExceptionSubscriber::class); + $this->reflectionSetValue($exceptionSubscriber, 'debug', false); + + $client = $this->getClient(); + + $client->request('POST', '/api/books/is-granted', [], [], [], \json_encode([ + 'title' => 'TITLE_NEW_1', + 'description' => 'DESCRIPTION_NEW_1', + 'author' => 2, + 'category' => 2, + ])); + $response = $client->getResponse(); + $this->assertEquals($response->getStatusCode(), 403); + } public function providerPatchTitle() { return [ diff --git a/tests/ProjectTest/Controller/Api/BookController.php b/tests/ProjectTest/Controller/Api/BookController.php index e6e99a4..72aecd9 100644 --- a/tests/ProjectTest/Controller/Api/BookController.php +++ b/tests/ProjectTest/Controller/Api/BookController.php @@ -8,6 +8,8 @@ use GollumSF\RestBundle\Annotation\Validate; use GollumSF\RestBundle\Model\StaticArrayApiList; use GollumSF\RestBundle\Search\ApiSearchInterface; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Test\GollumSF\RestBundle\ProjectTest\Entity\Book; @@ -36,7 +38,7 @@ public function find(Book $book) { /** * @Route("", methods={"POST"}) * @Unserialize("book", groups="book_post") - * @Validate({ "book_post" }) + * @Validate("book_post") * @Serialize(groups="book_get", code=Response::HTTP_CREATED) * */ @@ -44,6 +46,30 @@ public function post(Book $book) { return $book; } + /** + * @Route("/is-granted", methods={"POST"}) + * @IsGranted("IS_AUTHENTICATED_FULLY") + * @Unserialize("book", groups="book_post") + * @Validate("book_post") + * @Serialize(groups="book_get", code=Response::HTTP_CREATED) + * + */ + public function postDenyIsGranted(Book $book) { + return $book; + } + + /** + * @Route("/security", methods={"POST"}) + * @Security("is_granted('AUTHENTICATED_FULLY')") + * @Unserialize("book", groups="book_post") + * @Validate("book_post") + * @Serialize(groups="book_get", code=Response::HTTP_CREATED) + * + */ + public function postDenySecurity(Book $book) { + return $book; + } + /** * @Route("/{id}", methods={"PUT"}) * @Unserialize("book", groups="book_put") diff --git a/tests/ProjectTest/Resources/config/config.yaml b/tests/ProjectTest/Resources/config/config.yaml index 96d9de1..4d22fb0 100644 --- a/tests/ProjectTest/Resources/config/config.yaml +++ b/tests/ProjectTest/Resources/config/config.yaml @@ -39,4 +39,17 @@ services: tags: ['controller.service_arguments'] Test\GollumSF\RestBundle\ProjectTest\DataFixtures\: - resource: '%kernel.project_dir%/tests/ProjectTest/DataFixtures' \ No newline at end of file + resource: '%kernel.project_dir%/tests/ProjectTest/DataFixtures' + +security: + providers: + in_memory: + memory: + users: + test_user: { password: test } + firewalls: + main: + anonymous: true + http_basic: + realm: 'Secured Demo Area' + provider: in_memory \ No newline at end of file diff --git a/tests/Unit/EventSubscriber/SerializerSubscriberTest.php b/tests/Unit/EventSubscriber/SerializerSubscriberTest.php index ad0e334..6e2ae5b 100644 --- a/tests/Unit/EventSubscriber/SerializerSubscriberTest.php +++ b/tests/Unit/EventSubscriber/SerializerSubscriberTest.php @@ -15,7 +15,6 @@ use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; -use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ViewEvent; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -118,11 +117,11 @@ class SerializerSubscriberTest extends TestCase { public function testGetSubscribedEvents() { $this->assertEquals(SerializerSubscriber::getSubscribedEvents(), [ - KernelEvents::CONTROLLER => [ - ['onKernelController', -1], + KernelEvents::CONTROLLER_ARGUMENTS => [ + ['onKernelControllerArguments', -10], ], KernelEvents::VIEW => [ - ['onKernelView', -1], + ['onKernelView', -10], ], KernelEvents::EXCEPTION => [ ['onKernelValidateException', 257], @@ -130,7 +129,7 @@ public function testGetSubscribedEvents() { ]); } - public function provideOnKernelControllerSuccess() { + public function provideonKernelControllerArgumentsSuccess() { return [ [ 'POST', [], [ 'post' ], \stdClass::class ], [ 'post', [], [ 'post' ], \stdClass::class ], @@ -146,9 +145,9 @@ public function provideOnKernelControllerSuccess() { } /** - * @dataProvider provideOnKernelControllerSuccess + * @dataProvider provideonKernelControllerArgumentsSuccess */ - public function testOnKernelControllerSuccess($method, $groups, $groupResults, $class) { + public function testonKernelControllerArgumentsSuccess($method, $groups, $groupResults, $class) { $serializer = $this->getMockBuilder(StubSerializer::class)->getMockForAbstractClass(); $kernel = $this->getMockBuilder(KernelInterface::class)->getMockForAbstractClass(); @@ -165,7 +164,7 @@ public function testOnKernelControllerSuccess($method, $groups, $groupResults, $ 'groups' => $groups ]); - $event = new ControllerEvent($kernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST); + $event = new ControllerArgumentsEvent($kernel, $controller, [], $request, HttpKernelInterface::MASTER_REQUEST); $request ->expects($this->once()) @@ -206,12 +205,12 @@ public function testOnKernelControllerSuccess($method, $groups, $groupResults, $ $serializer ); - $serializerSubscriber->onKernelController($event); + $serializerSubscriber->onKernelControllerArguments($event); $this->assertEquals($serializerSubscriber->groups, $groupResults); } - public function testOnKernelControllerNoClassNoEntity() { + public function testonKernelControllerArgumentsNoClassNoEntity() { $serializer = $this->getMockBuilder(StubSerializer::class)->getMockForAbstractClass(); $kernel = $this->getMockBuilder(KernelInterface::class)->getMockForAbstractClass(); @@ -227,7 +226,7 @@ public function testOnKernelControllerNoClassNoEntity() { 'groups' => [] ]); - $event = new ControllerEvent($kernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST); + $event = new ControllerArgumentsEvent($kernel, $controller, [], $request, HttpKernelInterface::MASTER_REQUEST); $request ->expects($this->once()) @@ -265,11 +264,11 @@ public function testOnKernelControllerNoClassNoEntity() { $this->expectException(\LogicException::class); - $serializerSubscriber->onKernelController($event); + $serializerSubscriber->onKernelControllerArguments($event); } - public function testOnKernelControllerNoEntity() { + public function testonKernelControllerArgumentsNoEntity() { $serializer = $this->getMockBuilder(StubSerializer::class)->getMockForAbstractClass(); $kernel = $this->getMockBuilder(KernelInterface::class)->getMockForAbstractClass(); @@ -285,7 +284,7 @@ public function testOnKernelControllerNoEntity() { 'groups' => [] ]); - $event = new ControllerEvent($kernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST); + $event = new ControllerArgumentsEvent($kernel, $controller, [], $request, HttpKernelInterface::MASTER_REQUEST); $request ->expects($this->once()) @@ -323,10 +322,10 @@ public function testOnKernelControllerNoEntity() { $this->expectException(BadRequestHttpException::class); - $serializerSubscriber->onKernelController($event); + $serializerSubscriber->onKernelControllerArguments($event); } - public function providerOnKernelControllerSave() { + public function provideronKernelControllerArgumentsSave() { return [ [true, true, true ], [true, false, false ], @@ -336,9 +335,9 @@ public function providerOnKernelControllerSave() { } /** - * @dataProvider providerOnKernelControllerSave + * @dataProvider provideronKernelControllerArgumentsSave */ - public function testOnKernelControllerSave($isEntity, $save, $called) { + public function testonKernelControllerArgumentsSave($isEntity, $save, $called) { $serializer = $this->getMockBuilder(StubSerializer::class)->getMockForAbstractClass(); $em = $this->getMockForAbstractClass(ObjectManager::class); @@ -354,8 +353,8 @@ public function testOnKernelControllerSave($isEntity, $save, $called) { $entity = new \stdClass(); $controller = function () {}; - - $event = new ControllerEvent($kernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST); + + $event = new ControllerArgumentsEvent($kernel, $controller, [], $request, HttpKernelInterface::MASTER_REQUEST); $serializerSubscriber = new SerializerSubscriberOnKernelControllerArgumentsTestSave( $serializer, @@ -419,7 +418,7 @@ public function testOnKernelControllerSave($isEntity, $save, $called) { ; } - $serializerSubscriber->onKernelController($event); + $serializerSubscriber->onKernelControllerArguments($event); } public function providerUnserializeSuccess() { diff --git a/tests/Unit/Request/ParamConverter/PostRestParamConverterTest.php b/tests/Unit/Request/ParamConverter/PostRestParamConverterTest.php index a8cda45..f6a8b1c 100644 --- a/tests/Unit/Request/ParamConverter/PostRestParamConverterTest.php +++ b/tests/Unit/Request/ParamConverter/PostRestParamConverterTest.php @@ -8,12 +8,14 @@ use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\DoctrineParamConverter; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; +use Symfony\Component\Serializer\SerializerInterface; class PostRestParamConverterTest extends TestCase { public function providerApply() { return [ - [ new Unserialize(['name' => 'NAME']), 'NAME', null, true ], [ new Unserialize(['name' => 'NAME']), 'BAD_NAME', null, false ], [ new Unserialize(['name' => 'NAME']), 'NAME', 'value', false ], ]; @@ -24,6 +26,7 @@ public function providerApply() { */ public function testApply($annotation, $configurationName, $requestValue, $result) { + $serializer = $this->getMockForAbstractClass(SerializerInterface::class); $attributes = $this->getMockBuilder(ParameterBag::class) ->disableOriginalConstructor() ->getMock() @@ -63,27 +66,240 @@ public function testApply($annotation, $configurationName, $requestValue, $resul ->with($configurationName) ->willReturn($requestValue) ; - if (!$requestValue) { - $attributes - ->expects($this->at(2)) - ->method('set') - ->with('_'.Unserialize::ALIAS_NAME.'_class') - ->willReturn(\stdClass::class) - ; - } } - $postRestParamConverter = new PostRestParamConverter(); + $postRestParamConverter = new PostRestParamConverter($serializer); $this->assertEquals( $postRestParamConverter->apply($request, $configuration), $result ); } + public function testApplyDeserialize() { + $serializer = $this->getMockForAbstractClass(SerializerInterface::class); + $annotation = new Unserialize(['name' => 'NAME']); + $configurationName = 'NAME'; + $entity = new \stdClass(); + + $attributes = $this->getMockBuilder(ParameterBag::class) + ->disableOriginalConstructor() + ->getMock() + ; + + $request = $this->getMockBuilder(Request::class)->disableOriginalConstructor() + ->getMock() + ; + $request->attributes = $attributes; + + $configuration = $this->getMockBuilder(ParamConverter::class) + ->disableOriginalConstructor() + ->getMock() + ; + + $configuration + ->expects($this->at(0)) + ->method('getName') + ->willReturn($configurationName) + ; + $configuration + ->method('getClass') + ->willReturn(\stdClass::class) + ; + + $attributes + ->expects($this->at(0)) + ->method('get') + ->with('_'.Unserialize::ALIAS_NAME) + ->willReturn($annotation) + ; + $attributes + ->expects($this->at(1)) + ->method('get') + ->with($configurationName) + ->willReturn(null) + ; + $attributes + ->expects($this->at(2)) + ->method('get') + ->with($configurationName) + ->willReturn(null) + ; + $attributes + ->expects($this->at(3)) + ->method('set') + ->with($configurationName, $entity) + ; + $attributes + ->expects($this->at(4)) + ->method('set') + ->with('_'.Unserialize::ALIAS_NAME.'_class') + ; + + $request + ->expects($this->once()) + ->method('getContent') + ->willReturn(['CONTENT']) + ; + $serializer + ->expects($this->once()) + ->method('deserialize') + ->with(['CONTENT'], \stdClass::class, 'json',[ + 'groups' => [], + ]) + ->willReturn($entity) + ; + + $postRestParamConverter = new PostRestParamConverter($serializer); + + $this->assertEquals( + $postRestParamConverter->apply($request, $configuration), true + ); + } + + public function testApplyDeserializeMissingConstructorArgumentsException() { + + $serializer = $this->getMockForAbstractClass(SerializerInterface::class); + $annotation = new Unserialize(['name' => 'NAME']); + $configurationName = 'NAME'; + $entity = new \stdClass(); + + $attributes = $this->getMockBuilder(ParameterBag::class) + ->disableOriginalConstructor() + ->getMock() + ; + + $request = $this->getMockBuilder(Request::class)->disableOriginalConstructor() + ->getMock() + ; + $request->attributes = $attributes; + + $configuration = $this->getMockBuilder(ParamConverter::class) + ->disableOriginalConstructor() + ->getMock() + ; + + $configuration + ->expects($this->at(0)) + ->method('getName') + ->willReturn($configurationName) + ; + $configuration + ->method('getClass') + ->willReturn(\stdClass::class) + ; + + $attributes + ->expects($this->at(0)) + ->method('get') + ->with('_'.Unserialize::ALIAS_NAME) + ->willReturn($annotation) + ; + $attributes + ->expects($this->at(1)) + ->method('get') + ->with($configurationName) + ->willReturn(null) + ; + $attributes + ->expects($this->at(2)) + ->method('get') + ->with($configurationName) + ->willReturn(null) + ; + + $request + ->expects($this->once()) + ->method('getContent') + ->willReturn(['CONTENT']) + ; + $serializer + ->expects($this->once()) + ->method('deserialize') + ->with(['CONTENT'], \stdClass::class, 'json',[ + 'groups' => [], + ]) + ->willThrowException(new MissingConstructorArgumentsException()) + ; + + $this->expectException(BadRequestHttpException::class); + $postRestParamConverter = new PostRestParamConverter($serializer); + $postRestParamConverter->apply($request, $configuration); + } + + public function testApplyDeserializeUnexpectedValueException() { + + $serializer = $this->getMockForAbstractClass(SerializerInterface::class); + $annotation = new Unserialize(['name' => 'NAME']); + $configurationName = 'NAME'; + $entity = new \stdClass(); + + $attributes = $this->getMockBuilder(ParameterBag::class) + ->disableOriginalConstructor() + ->getMock() + ; + + $request = $this->getMockBuilder(Request::class)->disableOriginalConstructor() + ->getMock() + ; + $request->attributes = $attributes; + + $configuration = $this->getMockBuilder(ParamConverter::class) + ->disableOriginalConstructor() + ->getMock() + ; + + $configuration + ->expects($this->at(0)) + ->method('getName') + ->willReturn($configurationName) + ; + $configuration + ->method('getClass') + ->willReturn(\stdClass::class) + ; + + $attributes + ->expects($this->at(0)) + ->method('get') + ->with('_'.Unserialize::ALIAS_NAME) + ->willReturn($annotation) + ; + $attributes + ->expects($this->at(1)) + ->method('get') + ->with($configurationName) + ->willReturn(null) + ; + $attributes + ->expects($this->at(2)) + ->method('get') + ->with($configurationName) + ->willReturn(null) + ; + + $request + ->expects($this->once()) + ->method('getContent') + ->willReturn(['CONTENT']) + ; + $serializer + ->expects($this->once()) + ->method('deserialize') + ->with(['CONTENT'], \stdClass::class, 'json',[ + 'groups' => [], + ]) + ->willThrowException(new \UnexpectedValueException()) + ; + + $this->expectException(BadRequestHttpException::class); + $postRestParamConverter = new PostRestParamConverter($serializer); + $postRestParamConverter->apply($request, $configuration); + } public function testApplyDoctrineParamConverter() { + $serializer = $this->getMockForAbstractClass(SerializerInterface::class); $annotation = new Unserialize(['name' => 'NAME']); $configurationName = 'NAME'; @@ -109,24 +325,24 @@ public function testApplyDoctrineParamConverter() { ; $configuration ->expects($this->at(1)) + ->method('getClass') + ->willReturn(\stdClass::class) + ; + $configuration + ->expects($this->at(2)) ->method('isOptional') ->willReturn(false) ; $configuration - ->expects($this->at(2)) + ->expects($this->at(3)) ->method('setIsOptional') ->with(true) ; $configuration - ->expects($this->at(3)) + ->expects($this->at(4)) ->method('setIsOptional') ->with(false) ; - $configuration - ->expects($this->at(2)) - ->method('getClass') - ->willReturn(\stdClass::class) - ; $attributes ->expects($this->at(0)) @@ -143,6 +359,12 @@ public function testApplyDoctrineParamConverter() { ; $attributes ->expects($this->at(2)) + ->method('get') + ->with($configurationName) + ->willReturn(new \stdClass()) + ; + $attributes + ->expects($this->at(3)) ->method('set') ->with('_'.Unserialize::ALIAS_NAME.'_class') ->willReturn(\stdClass::class) @@ -162,7 +384,7 @@ public function testApplyDoctrineParamConverter() { ->with($request, $configuration) ; - $postRestParamConverter = new PostRestParamConverter(); + $postRestParamConverter = new PostRestParamConverter($serializer); $postRestParamConverter->setDoctrineParamConverter($doctrineParamConverter); $this->assertTrue( @@ -172,7 +394,8 @@ public function testApplyDoctrineParamConverter() { public function testSupports() { - $postRestParamConverter = new PostRestParamConverter(); + $serializer = $this->getMockForAbstractClass(SerializerInterface::class); + $postRestParamConverter = new PostRestParamConverter($serializer); $this->assertTrue( $postRestParamConverter->supports(