From 7f598ba30ed09bdead08a7f00049bebad94111e4 Mon Sep 17 00:00:00 2001 From: Niels Nijens Date: Mon, 10 Jun 2024 16:06:44 +0200 Subject: [PATCH 1/2] Add loading of validateBeforeFirewall property in x-openapi-bundle schema extension to RouteLoader --- src/Routing/RouteContext.php | 2 ++ src/Routing/RouteLoader.php | 2 ++ tests/Functional/App/openapi.yaml | 28 ++++++++++++++++++++++++++++ tests/Routing/RouteLoaderTest.php | 2 ++ 4 files changed, 34 insertions(+) diff --git a/src/Routing/RouteContext.php b/src/Routing/RouteContext.php index b0ef8c0..41632c1 100644 --- a/src/Routing/RouteContext.php +++ b/src/Routing/RouteContext.php @@ -34,6 +34,8 @@ final class RouteContext public const REQUEST_VALIDATE_QUERY_PARAMETERS = 'request_validate_query_parameters'; + public const REQUEST_VALIDATE_BEFORE_FIREWALL = 'request_validate_before_firewall'; + public const DESERIALIZATION_OBJECT = 'deserialization_object'; public const DESERIALIZATION_OBJECT_ARGUMENT_NAME = 'deserialization_object_argument_name'; diff --git a/src/Routing/RouteLoader.php b/src/Routing/RouteLoader.php index 3c30a18..6fda300 100755 --- a/src/Routing/RouteLoader.php +++ b/src/Routing/RouteLoader.php @@ -160,6 +160,8 @@ private function parseOpenapiBundleSpecificationExtension(stdClass $operation, a $defaults['_controller'] = $operation->{'x-openapi-bundle'}->controller; } + $openapiRouteContext[RouteContext::REQUEST_VALIDATE_BEFORE_FIREWALL] = $operation->{'x-openapi-bundle'}->validateBeforeFirewall ?? false; + if (isset($operation->{'x-openapi-bundle'}->deserializationObject)) { $openapiRouteContext[RouteContext::DESERIALIZATION_OBJECT] = $operation->{'x-openapi-bundle'}->deserializationObject; } diff --git a/tests/Functional/App/openapi.yaml b/tests/Functional/App/openapi.yaml index 5925028..1ea5d38 100644 --- a/tests/Functional/App/openapi.yaml +++ b/tests/Functional/App/openapi.yaml @@ -151,6 +151,34 @@ paths: type: integer format: int64 + /authenticate: + post: + operationId: authenticate + summary: Authenticate a user + x-openapi-bundle: + validateBeforeFirewall: true + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + username: + type: string + password: + type: string + required: + - username + - password + responses: + '204': + description: Successfully authenticated the user. + '401': + description: Invalid username/password supplied. + tags: + - pet + /authenticated/pets: post: x-openapi-bundle: diff --git a/tests/Routing/RouteLoaderTest.php b/tests/Routing/RouteLoaderTest.php index dca21b6..6082ab2 100755 --- a/tests/Routing/RouteLoaderTest.php +++ b/tests/Routing/RouteLoaderTest.php @@ -130,6 +130,7 @@ public function testCanLoadRoutesWithRouteContextForRequestParameterValidation() static::assertEquals( [ RouteContext::RESOURCE => __DIR__.'/../Resources/specifications/route-loader-request-validation.yaml', + RouteContext::REQUEST_VALIDATE_BEFORE_FIREWALL => false, RouteContext::REQUEST_BODY_REQUIRED => false, RouteContext::REQUEST_ALLOWED_CONTENT_TYPES => [], RouteContext::REQUEST_VALIDATE_QUERY_PARAMETERS => [ @@ -155,6 +156,7 @@ public function testCanLoadRoutesWithRouteContextForRequestBodyValidation(): voi static::assertSame( [ RouteContext::RESOURCE => __DIR__.'/../Resources/specifications/route-loader-request-validation.yaml', + RouteContext::REQUEST_VALIDATE_BEFORE_FIREWALL => false, RouteContext::REQUEST_BODY_REQUIRED => false, RouteContext::REQUEST_ALLOWED_CONTENT_TYPES => ['application/json'], RouteContext::REQUEST_VALIDATE_QUERY_PARAMETERS => [], From 308b58de99f02e0bf41c42c32b69a6c607d5f872 Mon Sep 17 00:00:00 2001 From: Niels Nijens Date: Mon, 10 Jun 2024 16:08:38 +0200 Subject: [PATCH 2/2] Add pre-firewall request body validation to RequestValidationSubscriber --- .../RequestValidationSubscriber.php | 18 +++++++++ tests/Functional/App/config.yaml | 4 +- .../JsonRequestBodyValidationTest.php | 39 +++++++++++++++++++ .../RequestValidationSubscriberTest.php | 1 + 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/Validation/EventSubscriber/RequestValidationSubscriber.php b/src/Validation/EventSubscriber/RequestValidationSubscriber.php index 75a32a3..669134b 100644 --- a/src/Validation/EventSubscriber/RequestValidationSubscriber.php +++ b/src/Validation/EventSubscriber/RequestValidationSubscriber.php @@ -37,6 +37,7 @@ public static function getSubscribedEvents(): array { return [ KernelEvents::REQUEST => [ + ['validateRequestBeforeFirewall', 10], ['validateRequest', 7], ], ]; @@ -47,6 +48,16 @@ public function __construct(ValidatorInterface $requestValidator) $this->requestValidator = $requestValidator; } + public function validateRequestBeforeFirewall(RequestEvent $event): void + { + $request = $event->getRequest(); + if ($this->isManagedRoute($request) === false || $this->isPreFirewallRequestValidationEnabled($request) === false) { + return; + } + + $this->validateRequest($event); + } + public function validateRequest(RequestEvent $event): void { $request = $event->getRequest(); @@ -64,4 +75,11 @@ private function isManagedRoute(Request $request): bool { return $request->attributes->has(RouteContext::REQUEST_ATTRIBUTE); } + + private function isPreFirewallRequestValidationEnabled(Request $request): bool + { + $routeContext = $request->attributes->get(RouteContext::REQUEST_ATTRIBUTE); + + return $routeContext[RouteContext::REQUEST_VALIDATE_BEFORE_FIREWALL] ?? false; + } } diff --git a/tests/Functional/App/config.yaml b/tests/Functional/App/config.yaml index 10bd353..99304e2 100644 --- a/tests/Functional/App/config.yaml +++ b/tests/Functional/App/config.yaml @@ -33,12 +33,12 @@ security: firewalls: main: - pattern: '^/api/authenticated' + pattern: '^/api/(authenticate|authenticated)' lazy: true stateless: true provider: users_in_memory json_login: - check_path: "/api/authenticated" + check_path: "/api/authenticate" username_path: email password_path: password diff --git a/tests/Functional/Validation/JsonRequestBodyValidationTest.php b/tests/Functional/Validation/JsonRequestBodyValidationTest.php index 2f28053..854358a 100644 --- a/tests/Functional/Validation/JsonRequestBodyValidationTest.php +++ b/tests/Functional/Validation/JsonRequestBodyValidationTest.php @@ -108,6 +108,45 @@ public function testCanReturnProblemDetailsJsonObjectForInvalidRequestBody(): vo ); } + public function testCanReturnProblemDetailsJsonObjectForInvalidRequestBodyBeforeFirewall(): void + { + $this->client->request( + Request::METHOD_POST, + '/api/authenticate', + [], + [], + [ + 'CONTENT_TYPE' => 'application/json', + ], + '{}' + ); + + $expectedJsonResponseBody = [ + 'type' => 'about:blank', + 'title' => 'The request body contains errors.', + 'status' => 400, + 'detail' => 'Validation of JSON request body failed.', + 'violations' => [ + [ + 'constraint' => 'required', + 'message' => 'The property username is required', + 'property' => 'username', + ], + [ + 'constraint' => 'required', + 'message' => 'The property password is required', + 'property' => 'password', + ], + ], + ]; + + static::assertResponseStatusCodeSame(Response::HTTP_BAD_REQUEST); + static::assertJsonStringEqualsJsonString( + json_encode($expectedJsonResponseBody), + $this->client->getResponse()->getContent() + ); + } + public function testCannotReturnProblemDetailsJsonObjectWithoutRequiredRequestBody(): void { $this->client->request( diff --git a/tests/Validation/EventSubscriber/RequestValidationSubscriberTest.php b/tests/Validation/EventSubscriber/RequestValidationSubscriberTest.php index 846aaf0..59ada2c 100644 --- a/tests/Validation/EventSubscriber/RequestValidationSubscriberTest.php +++ b/tests/Validation/EventSubscriber/RequestValidationSubscriberTest.php @@ -54,6 +54,7 @@ public function testCanReturnSubscribedEvents(): void $this->assertSame( [ KernelEvents::REQUEST => [ + ['validateRequestBeforeFirewall', 10], ['validateRequest', 7], ], ],