From aed4725611b461a8a66522ce25e1127638e5650f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klatt?= Date: Tue, 19 Nov 2024 09:33:29 +0100 Subject: [PATCH 1/8] ACP-4403 Added Request against OpenApi schema validation. --- composer.json | 2 + resources/api/openapi.yml | 8 +- .../Glue/AppKernel/AppKernelConfig.php | 8 + .../Glue/AppKernel/AppKernelFactory.php | 9 + .../AppGlueRequestSchemaValidatorPlugin.php | 24 ++ .../OpenApiRequestSchemaValidator.php | 75 +++++ .../Shared/AppKernel/AppKernelConstants.php | 10 + ...ppGlueRequestSchemaValidatorPluginTest.php | 259 ++++++++++++++++++ .../Fixtures/OpenApi/valid-openapi-schema.yml | 128 +++++++++ .../AppKernel/_support/AppKernelTester.php | 1 + 10 files changed, 520 insertions(+), 4 deletions(-) create mode 100644 src/Spryker/Glue/AppKernel/Plugin/GlueApplication/AppGlueRequestSchemaValidatorPlugin.php create mode 100644 src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php create mode 100644 tests/SprykerTest/Glue/AppKernel/Plugin/GlueApplication/AppGlueRequestSchemaValidatorPluginTest.php create mode 100644 tests/SprykerTest/Glue/AppKernel/_data/Fixtures/OpenApi/valid-openapi-schema.yml diff --git a/composer.json b/composer.json index 0946c80..4fa3d96 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,8 @@ "license": "proprietary", "require": { "php": ">=8.1", + "guzzlehttp/psr7": "^2.7", + "league/openapi-psr7-validator": "^0.22.0", "spryker/app-kernel-extension": "^1.0.0", "spryker/glue-application-extension": "^1.0.0", "spryker/kernel": "^3.30.0", diff --git a/resources/api/openapi.yml b/resources/api/openapi.yml index f364b56..258c3cd 100644 --- a/resources/api/openapi.yml +++ b/resources/api/openapi.yml @@ -5,15 +5,15 @@ info: name: Spryker url: 'https://spryker.com/app-composition-platform/' email: support@spryker.com - title: 'Hello World' + title: 'AppKernel API' license: name: MIT servers: - - url: 'http://glue-backend.hello-world.spryker.local/' + - url: 'http://glue-backend.app-kernel.spryker.local/' description: 'Local development endpoint' - - url: 'https://api.hello-world-testing.aop.demo-spryker.com/' + - url: 'https://api.app-kernel-testing.aop.demo-spryker.com/' description: 'Testing' - - url: 'https://api.hello-world-staging.aop.demo-spryker.com/' + - url: 'https://api.app-kernel-staging.aop.demo-spryker.com/' description: 'Staging' paths: '/private/configure': diff --git a/src/Spryker/Glue/AppKernel/AppKernelConfig.php b/src/Spryker/Glue/AppKernel/AppKernelConfig.php index c596419..9e34aec 100644 --- a/src/Spryker/Glue/AppKernel/AppKernelConfig.php +++ b/src/Spryker/Glue/AppKernel/AppKernelConfig.php @@ -8,6 +8,7 @@ namespace Spryker\Glue\AppKernel; use Spryker\Glue\Kernel\AbstractBundleConfig; +use Spryker\Shared\AppKernel\AppKernelConstants; class AppKernelConfig extends AbstractBundleConfig { @@ -52,7 +53,14 @@ class AppKernelConfig extends AbstractBundleConfig public const RESPONSE_MESSAGE_DISCONNECT_ERROR = 'Disconnecting error.'; /** + * @api + * * @var string */ public const ERROR_CODE_PAYMENT_DISCONNECTION_TENANT_IDENTIFIER_MISSING = '20000'; + + public function getOpenApiSchemaPath(): ?string + { + return $this->getConfig()->hasKey(AppKernelConstants::OPEN_API_SCHEMA_PATH) ? $this->getConfig()->get(AppKernelConstants::OPEN_API_SCHEMA_PATH) : null; + } } diff --git a/src/Spryker/Glue/AppKernel/AppKernelFactory.php b/src/Spryker/Glue/AppKernel/AppKernelFactory.php index 62a4e36..2158fd9 100644 --- a/src/Spryker/Glue/AppKernel/AppKernelFactory.php +++ b/src/Spryker/Glue/AppKernel/AppKernelFactory.php @@ -17,12 +17,16 @@ use Spryker\Glue\AppKernel\Validator\BodyStructureValidator; use Spryker\Glue\AppKernel\Validator\ConfigurationValidator; use Spryker\Glue\AppKernel\Validator\HeaderValidator; +use Spryker\Glue\AppKernel\Validator\OpenApiRequestSchemaValidator; use Spryker\Glue\AppKernel\Validator\RequestValidator; use Spryker\Glue\AppKernel\Validator\RequestValidatorInterface; use Spryker\Glue\Kernel\Backend\AbstractFactory; use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Validator\ValidatorInterface; +/** + * @method \Spryker\Glue\AppKernel\AppKernelConfig getConfig() + */ class AppKernelFactory extends AbstractFactory { public function createHeaderValidator(): RequestValidatorInterface @@ -109,4 +113,9 @@ public function getApiRequestDisconnectValidatorPlugins(): array { return $this->getProvidedDependency(AppKernelDependencyProvider::PLUGINS_REQUEST_DISCONNECT_VALIDATOR); } + + public function createOpenApiRequestSchemaValidator(): OpenApiRequestSchemaValidator + { + return new OpenApiRequestSchemaValidator($this->getConfig()); + } } diff --git a/src/Spryker/Glue/AppKernel/Plugin/GlueApplication/AppGlueRequestSchemaValidatorPlugin.php b/src/Spryker/Glue/AppKernel/Plugin/GlueApplication/AppGlueRequestSchemaValidatorPlugin.php new file mode 100644 index 0000000..fc18d29 --- /dev/null +++ b/src/Spryker/Glue/AppKernel/Plugin/GlueApplication/AppGlueRequestSchemaValidatorPlugin.php @@ -0,0 +1,24 @@ +getFactory()->createOpenApiRequestSchemaValidator()->validate($glueRequestTransfer); + } +} diff --git a/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php b/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php new file mode 100644 index 0000000..a387238 --- /dev/null +++ b/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php @@ -0,0 +1,75 @@ +setIsValid(true); + + $openApiSchemaPath = $this->appKernelConfig->getOpenApiSchemaPath(); + + if (!$openApiSchemaPath) { + return $glueRequestValidationTransfer; + } + + // Converting the HTTP request to PSR7 request + $psr7Request = new Request( + $glueRequestTransfer->getMethod() ?? '', + $glueRequestTransfer->getPath() ?? '', + $glueRequestTransfer->getMeta(), + $glueRequestTransfer->getContent(), + ); + + // Validate the request + $validator = (new ValidatorBuilder()) + ->fromYamlFile($openApiSchemaPath) + ->getRequestValidator(); + + try { + $validator->validate($psr7Request); + } catch (Throwable $throwable) { + $glueErrorTransfer = new GlueErrorTransfer(); + $glueErrorTransfer + ->setMessage($this->getMessageFromThrowable($throwable)); + + $glueRequestValidationTransfer + ->setIsValid(false) + ->addError($glueErrorTransfer) + ->setStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + + return $glueRequestValidationTransfer; + } + + return $glueRequestValidationTransfer; + } + + protected function getMessageFromThrowable(Throwable $throwable): string + { + return match (get_class($throwable)) { + InvalidBody::class => $throwable->getVerboseMessage(), + default => $throwable->getMessage(), + }; + } +} diff --git a/src/Spryker/Shared/AppKernel/AppKernelConstants.php b/src/Spryker/Shared/AppKernel/AppKernelConstants.php index 9f0f750..fab0a15 100644 --- a/src/Spryker/Shared/AppKernel/AppKernelConstants.php +++ b/src/Spryker/Shared/AppKernel/AppKernelConstants.php @@ -31,4 +31,14 @@ class AppKernelConstants * @var string */ public const APP_VERSION = 'APP:APP_VERSION'; + + /** + * Specification: + * - Path to an OpenAPI schema path that can be used for request validation. + * + * @api + * + * @var string + */ + public const OPEN_API_SCHEMA_PATH = 'APP:OPEN_API_SCHEMA_PATH'; } diff --git a/tests/SprykerTest/Glue/AppKernel/Plugin/GlueApplication/AppGlueRequestSchemaValidatorPluginTest.php b/tests/SprykerTest/Glue/AppKernel/Plugin/GlueApplication/AppGlueRequestSchemaValidatorPluginTest.php new file mode 100644 index 0000000..2f3c38a --- /dev/null +++ b/tests/SprykerTest/Glue/AppKernel/Plugin/GlueApplication/AppGlueRequestSchemaValidatorPluginTest.php @@ -0,0 +1,259 @@ + codecept_data_dir('Fixtures/OpenApi/valid-openapi-schema.yml'), + ]); + + $appKernelFactory = Stub::make(AppKernelFactory::class, [ + 'getConfig' => $appKernelConfigStub, + ]); + + $appGlueRequestSchemaValidatorPlugin = new AppGlueRequestSchemaValidatorPlugin(); + $appGlueRequestSchemaValidatorPlugin->setFactory($appKernelFactory); + + $glueRequestTransfer = new GlueRequestTransfer(); + $glueRequestTransfer + ->setMethod('POST') + ->setPath('/existing-endpoint') + ->setMeta([ + 'X-Tenant-Identifier' => 'tenant-identifier', + 'Content-Type' => 'application/json', + ]) + ->setContent('{"data": {"attributes": {"foo": "bar"}}}'); + + // Act + $glueRequestValidationTransfer = $appGlueRequestSchemaValidatorPlugin->validate($glueRequestTransfer); + + // Assert + $this->assertTrue($glueRequestValidationTransfer->getIsValid()); + } + + public function testGivenNoOpenApiSchemaFileDefinedWhenTheValidationIsExecutedThenAValidValidationResponseIsReturned(): void + { + // Arrange + $appGlueRequestSchemaValidatorPlugin = new AppGlueRequestSchemaValidatorPlugin(); + + $glueRequestTransfer = new GlueRequestTransfer(); + + // Act + $glueRequestValidationTransfer = $appGlueRequestSchemaValidatorPlugin->validate($glueRequestTransfer); + + // Assert + $this->assertTrue($glueRequestValidationTransfer->getIsValid()); + } + + public function testGivenAValidOpenApiSchemaWhenTheRequestedPathIsNotDefinedThenAnInValidValidationResponseIsReturned(): void + { + // Arrange + $appKernelConfigStub = Stub::make(AppKernelConfig::class, [ + 'getOpenApiSchemaPath' => codecept_data_dir('Fixtures/OpenApi/valid-openapi-schema.yml'), + ]); + + $appKernelFactory = Stub::make(AppKernelFactory::class, [ + 'getConfig' => $appKernelConfigStub, + ]); + + $appGlueRequestSchemaValidatorPlugin = new AppGlueRequestSchemaValidatorPlugin(); + $appGlueRequestSchemaValidatorPlugin->setFactory($appKernelFactory); + + $glueRequestTransfer = new GlueRequestTransfer(); + $glueRequestTransfer + ->setMethod('GET') + ->setPath('/non-existing-endpoint'); + + // Act + $glueRequestValidationTransfer = $appGlueRequestSchemaValidatorPlugin->validate($glueRequestTransfer); + + // Assert + $this->assertFalse($glueRequestValidationTransfer->getIsValid()); + $this->assertSame('OpenAPI spec contains no such operation [/non-existing-endpoint]', $glueRequestValidationTransfer->getErrors()[0]->getMessage()); + } + + public function testGivenAValidOpenApiSchemaWhenTheRequestDoesNotHaveARequiredHeaderThenAnInValidValidationResponseIsReturned(): void + { + // Arrange + $appKernelConfigStub = Stub::make(AppKernelConfig::class, [ + 'getOpenApiSchemaPath' => codecept_data_dir('Fixtures/OpenApi/valid-openapi-schema.yml'), + ]); + + $appKernelFactory = Stub::make(AppKernelFactory::class, [ + 'getConfig' => $appKernelConfigStub, + ]); + + $appGlueRequestSchemaValidatorPlugin = new AppGlueRequestSchemaValidatorPlugin(); + $appGlueRequestSchemaValidatorPlugin->setFactory($appKernelFactory); + + $glueRequestTransfer = new GlueRequestTransfer(); + $glueRequestTransfer + ->setMethod('GET') + ->setPath('/existing-endpoint'); + + // Act + $glueRequestValidationTransfer = $appGlueRequestSchemaValidatorPlugin->validate($glueRequestTransfer); + + // Assert + $this->assertFalse($glueRequestValidationTransfer->getIsValid()); + $this->assertSame('Missing required header "X-Tenant-Identifier" for Request [get /existing-endpoint]', $glueRequestValidationTransfer->getErrors()[0]->getMessage()); + } + + public function testGivenAValidOpenApiSchemaWhenTheRequestDoesNotHaveTheContentTypeHeaderSetThenAnInValidValidationResponseIsReturned(): void + { + // Arrange + $appKernelConfigStub = Stub::make(AppKernelConfig::class, [ + 'getOpenApiSchemaPath' => codecept_data_dir('Fixtures/OpenApi/valid-openapi-schema.yml'), + ]); + + $appKernelFactory = Stub::make(AppKernelFactory::class, [ + 'getConfig' => $appKernelConfigStub, + ]); + + $appGlueRequestSchemaValidatorPlugin = new AppGlueRequestSchemaValidatorPlugin(); + $appGlueRequestSchemaValidatorPlugin->setFactory($appKernelFactory); + + $glueRequestTransfer = new GlueRequestTransfer(); + $glueRequestTransfer + ->setMethod('POST') + ->setPath('/existing-endpoint') + ->setMeta([ + 'X-Tenant-Identifier' => 'tenant-identifier', + ]); + + // Act + $glueRequestValidationTransfer = $appGlueRequestSchemaValidatorPlugin->validate($glueRequestTransfer); + + // Assert + $this->assertFalse($glueRequestValidationTransfer->getIsValid()); + $this->assertSame('Missing required header "Content-Type" for Request [post /existing-endpoint]', $glueRequestValidationTransfer->getErrors()[0]->getMessage()); + } + + public function testGivenAValidOpenApiSchemaWhenTheRequestDoesNotHaveABodyThenAnInValidValidationResponseIsReturned(): void + { + // Arrange + $appKernelConfigStub = Stub::make(AppKernelConfig::class, [ + 'getOpenApiSchemaPath' => codecept_data_dir('Fixtures/OpenApi/valid-openapi-schema.yml'), + ]); + + $appKernelFactory = Stub::make(AppKernelFactory::class, [ + 'getConfig' => $appKernelConfigStub, + ]); + + $appGlueRequestSchemaValidatorPlugin = new AppGlueRequestSchemaValidatorPlugin(); + $appGlueRequestSchemaValidatorPlugin->setFactory($appKernelFactory); + + $glueRequestTransfer = new GlueRequestTransfer(); + $glueRequestTransfer + ->setMethod('POST') + ->setPath('/existing-endpoint') + ->setMeta([ + 'X-Tenant-Identifier' => 'tenant-identifier', + 'Content-Type' => 'application/json', + ]); + + // Act + $glueRequestValidationTransfer = $appGlueRequestSchemaValidatorPlugin->validate($glueRequestTransfer); + + // Assert + $this->assertFalse($glueRequestValidationTransfer->getIsValid()); + $this->assertSame('JSON parsing failed with "Syntax error" for Request [post /existing-endpoint]', $glueRequestValidationTransfer->getErrors()[0]->getMessage()); + } + + public function testGivenAValidOpenApiSchemaWhenTheRequestDoesNotHaveARequiredFieldThenAnInValidValidationResponseIsReturned(): void + { + // Arrange + $appKernelConfigStub = Stub::make(AppKernelConfig::class, [ + 'getOpenApiSchemaPath' => codecept_data_dir('Fixtures/OpenApi/valid-openapi-schema.yml'), + ]); + + $appKernelFactory = Stub::make(AppKernelFactory::class, [ + 'getConfig' => $appKernelConfigStub, + ]); + + $appGlueRequestSchemaValidatorPlugin = new AppGlueRequestSchemaValidatorPlugin(); + $appGlueRequestSchemaValidatorPlugin->setFactory($appKernelFactory); + + $glueRequestTransfer = new GlueRequestTransfer(); + $glueRequestTransfer + ->setMethod('POST') + ->setPath('/existing-endpoint') + ->setMeta([ + 'X-Tenant-Identifier' => 'tenant-identifier', + 'Content-Type' => 'application/json', + ]) + ->setContent('{"key": "value"}'); + + // Act + $glueRequestValidationTransfer = $appGlueRequestSchemaValidatorPlugin->validate($glueRequestTransfer); + + // Assert + $this->assertFalse($glueRequestValidationTransfer->getIsValid()); + $this->assertSame('Body does not match schema for content-type "application/json" for Request [post /existing-endpoint]. [Keyword validation failed: Required property \'data\' must be present in the object in data]', $glueRequestValidationTransfer->getErrors()[0]->getMessage()); + } + + public function testGivenAValidOpenApiSchemaWhenTheRequestDoesNotHaveARequiredFieldInTheSecondLevelThenAnInValidValidationResponseIsReturned(): void + { + // Arrange + $appKernelConfigStub = Stub::make(AppKernelConfig::class, [ + 'getOpenApiSchemaPath' => codecept_data_dir('Fixtures/OpenApi/valid-openapi-schema.yml'), + ]); + + $appKernelFactory = Stub::make(AppKernelFactory::class, [ + 'getConfig' => $appKernelConfigStub, + ]); + + $appGlueRequestSchemaValidatorPlugin = new AppGlueRequestSchemaValidatorPlugin(); + $appGlueRequestSchemaValidatorPlugin->setFactory($appKernelFactory); + + $glueRequestTransfer = new GlueRequestTransfer(); + $glueRequestTransfer + ->setMethod('POST') + ->setPath('/existing-endpoint') + ->setMeta([ + 'X-Tenant-Identifier' => 'tenant-identifier', + 'Content-Type' => 'application/json', + ]) + ->setContent('{"data": {"foo": "bar"}}'); + + // Act + $glueRequestValidationTransfer = $appGlueRequestSchemaValidatorPlugin->validate($glueRequestTransfer); + + // Assert + $this->assertFalse($glueRequestValidationTransfer->getIsValid()); + $this->assertSame('Body does not match schema for content-type "application/json" for Request [post /existing-endpoint]. [Keyword validation failed: Required property \'attributes\' must be present in the object in data->attributes]', $glueRequestValidationTransfer->getErrors()[0]->getMessage()); + } +} diff --git a/tests/SprykerTest/Glue/AppKernel/_data/Fixtures/OpenApi/valid-openapi-schema.yml b/tests/SprykerTest/Glue/AppKernel/_data/Fixtures/OpenApi/valid-openapi-schema.yml new file mode 100644 index 0000000..fde5653 --- /dev/null +++ b/tests/SprykerTest/Glue/AppKernel/_data/Fixtures/OpenApi/valid-openapi-schema.yml @@ -0,0 +1,128 @@ +openapi: 3.0.0 +info: + version: 1.1.0 + contact: + name: Spryker + url: 'https://spryker.com/app-composition-platform' + email: support@spryker.com + title: 'Hello World' + license: + name: MIT + url: 'https://opensource.org/licenses/MIT' +servers: + - url: 'http://glue-backend.hello-world.spryker.local' + description: 'Local development endpoint' + - url: 'https://api.hello-world-testing.aop.demo-spryker.com' + description: 'Testing' + - url: 'https://api.hello-world-staging.aop.demo-spryker.com' + description: 'Staging' + +paths: + '/existing-endpoint': + get: + operationId: 'getExistingEndpoint' + summary: 'Test' + parameters: + - $ref: '#/components/parameters/tenantIdentifier' + responses: + 200: + description: 'Expected response to a valid request.' + content: + application/json: + schema: + $ref: '#/components/schemas/Response' + 400: + description: 'Bad request.' + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorMessages' + default: + description: 'Expected response to a bad request.' + content: + text/plain: + schema: + type: string + post: + operationId: 'postExistingEndpoint' + summary: 'Test' + parameters: + - $ref: '#/components/parameters/tenantIdentifier' + requestBody: + description: 'Test' + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Request' + responses: + 200: + description: 'Expected response to a valid request.' + content: + application/json: + schema: + $ref: '#/components/schemas/Response' + 400: + description: 'Bad request.' + content: + application/json: + schema: + $ref: '#/components/schemas/ApiErrorMessages' + default: + description: 'Expected response to a bad request.' + content: + text/plain: + schema: + type: string + +components: + schemas: + Request: + properties: + data: + type: object + required: + - attributes + properties: + attributes: + type: object + required: + - foo + properties: + foo: + type: string + required: + - data + Response: + properties: + configuration: + type: object + properties: + # Add your properties here + isActive: + type: boolean + ApiErrorMessages: + properties: + data: + type: array + items: + $ref: '#/components/schemas/ApiErrorMessage' + ApiErrorMessage: + properties: + code: + type: string + detail: + type: string + status: + type: string + parameters: + tenantIdentifier: + name: X-Tenant-Identifier + in: header + required: true + description: 'Identifier of the Tenant.' + schema: + type: string + examples: + local_de: + value: 1234-5678-9012-3456 diff --git a/tests/SprykerTest/Glue/AppKernel/_support/AppKernelTester.php b/tests/SprykerTest/Glue/AppKernel/_support/AppKernelTester.php index 78793b7..2bd2f61 100644 --- a/tests/SprykerTest/Glue/AppKernel/_support/AppKernelTester.php +++ b/tests/SprykerTest/Glue/AppKernel/_support/AppKernelTester.php @@ -27,6 +27,7 @@ * @method void pause() * * @SuppressWarnings(\SprykerTest\Glue\AppKernel\PHPMD) + * @method \Spryker\Glue\AppKernel\AppKernelFactory getFactory() */ class AppKernelTester extends Actor { From 824e59878720ae8b08429247ea7859cb623b77c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klatt?= Date: Tue, 19 Nov 2024 09:47:05 +0100 Subject: [PATCH 2/8] ACP-4403 Fixed CI --- .../Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php b/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php index a387238..3bb2427 100644 --- a/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php +++ b/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php @@ -30,7 +30,7 @@ public function validate(GlueRequestTransfer $glueRequestTransfer): GlueRequestV $openApiSchemaPath = $this->appKernelConfig->getOpenApiSchemaPath(); - if (!$openApiSchemaPath) { + if ($openApiSchemaPath === null || $openApiSchemaPath === '' || $openApiSchemaPath === '0') { return $glueRequestValidationTransfer; } From fc9af53d2fff5f359f87ec6bb608abf1f87a5d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klatt?= Date: Tue, 19 Nov 2024 10:01:34 +0100 Subject: [PATCH 3/8] ACP-4403 Updated min version of PHP --- .github/workflows/ci.yml | 1 - composer.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f75d5e5..dab278f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,6 @@ jobs: fail-fast: false matrix: php-version: [ - '8.1', '8.2', ] steps: diff --git a/composer.json b/composer.json index 4fa3d96..e84485e 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "description": "AppKernel module", "license": "proprietary", "require": { - "php": ">=8.1", + "php": ">=8.2", "guzzlehttp/psr7": "^2.7", "league/openapi-psr7-validator": "^0.22.0", "spryker/app-kernel-extension": "^1.0.0", From 56789a6f8ea44fd1b37198450e976ec8a8313623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klatt?= Date: Tue, 19 Nov 2024 13:38:22 +0100 Subject: [PATCH 4/8] ACP-4403 Changed response code to Bad Request. --- .../Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php b/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php index 3bb2427..2314e14 100644 --- a/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php +++ b/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php @@ -57,7 +57,7 @@ public function validate(GlueRequestTransfer $glueRequestTransfer): GlueRequestV $glueRequestValidationTransfer ->setIsValid(false) ->addError($glueErrorTransfer) - ->setStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + ->setStatus(Response::HTTP_BAD_REQUEST); return $glueRequestValidationTransfer; } From 60701090819d0f441880367a0beb0cf60f61eeb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klatt?= Date: Wed, 20 Nov 2024 06:29:12 +0100 Subject: [PATCH 5/8] ACP-4403 Fixed Badge. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ca12483..60d87d4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AppKernel Module [![Latest Stable Version](https://poser.pugx.org/spryker/app-kernel/v/stable.svg)](https://packagist.org/packages/spryker/app-kernel) -[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.1-8892BF.svg)](https://php.net/) +[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%208.2-8892BF.svg)](https://php.net/) Provides SyncAPI and AsyncAPI schema files and the needed code to make the Mini-Framework an App. From a501eaf7fbfc79c0033debba3399b42f6ddab46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klatt?= Date: Wed, 20 Nov 2024 07:28:31 +0100 Subject: [PATCH 6/8] ACP-4403 Make the openapi schema valid. --- resources/api/openapi.yml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/resources/api/openapi.yml b/resources/api/openapi.yml index 258c3cd..69ca69b 100644 --- a/resources/api/openapi.yml +++ b/resources/api/openapi.yml @@ -8,17 +8,21 @@ info: title: 'AppKernel API' license: name: MIT + url: 'https://opensource.org/licenses/MIT' servers: - - url: 'http://glue-backend.app-kernel.spryker.local/' + - url: 'http://glue-backend.app-kernel.spryker.local' description: 'Local development endpoint' - - url: 'https://api.app-kernel-testing.aop.demo-spryker.com/' + - url: 'https://api.app-kernel-testing.aop.demo-spryker.com' description: 'Testing' - - url: 'https://api.app-kernel-staging.aop.demo-spryker.com/' + - url: 'https://api.app-kernel-staging.aop.demo-spryker.com' description: 'Staging' paths: '/private/configure': post: + operationId: 'configure' summary: 'Saves or updates Hello World App configuration between Tenants and this App.' + security: + - Bearer: [ ] parameters: - $ref: '#/components/parameters/tenantIdentifier' requestBody: @@ -49,8 +53,11 @@ paths: type: string '/private/disconnect': post: + operationId: 'disconnect' summary: 'Disconnects this App from a Tenants Application. Finds configuration and removes it.' + security: + - Bearer: [ ] parameters: - $ref: '#/components/parameters/tenantIdentifier' responses: @@ -69,6 +76,13 @@ paths: schema: type: string components: + securitySchemes: + Bearer: + type: apiKey + in: header + name: Authorization + description: >- + Enter the token with the `Bearer: ` prefix, e.g. "Bearer abcde12345". schemas: ConfigurationApiRequest: properties: From d19934b5e422ba1ce086fc39e74f954b946b9d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klatt?= Date: Thu, 21 Nov 2024 10:32:09 +0100 Subject: [PATCH 7/8] ACP-4403 Added docs for validation. --- README.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 60d87d4..24f8e0d 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,37 @@ composer require spryker/app-kernel config/Shared/config_default.php ``` -use Spryker\Shared\AppKernel\AppConstants; +use Spryker\Shared\AppKernel\AppKernelConstants; -$config[AppConstants::APP_IDENTIFIER] = getenv('APP_IDENTIFIER') ?: 'hello-world'; +$config[AppKernelConstants::APP_IDENTIFIER] = getenv('APP_IDENTIFIER') ?: 'hello-world'; +$config[AppKernelConstants::OPEN_API_SCHEMA_PATH] = 'path/to/your/openApiSchema.yml'; ``` +### Validating Requests against the OpenAPI Schema + +Low level validation can be done by using the `Spryker\Zed\AppKernel\Communication\Plugin\OpenApiSchemaValidatorPlugin` plugin. When this plugin is added to the GlueApplicationDependencyProvider all API requests against this App will be validated against the defined OpenAPI schema. + +To enable this, you need to have a well-defined OpenAPI schema file, and you need to add the `OpenApiSchemaValidatorPlugin` plugin to the `getRestApplicationPlugins` method in your GlueApplicationDependencyProvider. + +```php +use Spryker\Zed\AppKernel\Communication\Plugin\OpenApiSchemaValidatorPlugin; + +... + + protected function getRestApplicationPlugins(): array + { + return [ + new OpenApiSchemaValidatorPlugin(), + ]; + } + +... +``` + +Pay intention that this will be a hard validation that gets executed before any other code from your App gets executed. If the validation fails, the request will be rejected with a 400 Bad Request response with a proper message that explains what exactly is wrong in the request. + +Make sure you have tests for your API. + ### Testing the AppKernel You can test the AppKernel as usual with Codeception. Before that you need to run some commands: From 246111c3661e65cbf133ac37dd3f9175574de083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klatt?= Date: Mon, 25 Nov 2024 10:15:47 +0100 Subject: [PATCH 8/8] ACP-4047 Fixed CR comments. --- resources/api/openapi.yml | 8 ++++---- .../AppKernel/Validator/OpenApiRequestSchemaValidator.php | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/resources/api/openapi.yml b/resources/api/openapi.yml index 69ca69b..2500df0 100644 --- a/resources/api/openapi.yml +++ b/resources/api/openapi.yml @@ -78,11 +78,11 @@ paths: components: securitySchemes: Bearer: - type: apiKey - in: header - name: Authorization + type: http + scheme: bearer + bearerFormat: JWT description: >- - Enter the token with the `Bearer: ` prefix, e.g. "Bearer abcde12345". + Enter the token with the `Bearer ` prefix, e.g. "Bearer abcde12345". schemas: ConfigurationApiRequest: properties: diff --git a/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php b/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php index 2314e14..d746644 100644 --- a/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php +++ b/src/Spryker/Glue/AppKernel/Validator/OpenApiRequestSchemaValidator.php @@ -14,11 +14,14 @@ use League\OpenAPIValidation\PSR7\Exception\Validation\InvalidBody; use League\OpenAPIValidation\PSR7\ValidatorBuilder; use Spryker\Glue\AppKernel\AppKernelConfig; +use Spryker\Shared\Log\LoggerTrait; use Symfony\Component\HttpFoundation\Response; use Throwable; class OpenApiRequestSchemaValidator { + use LoggerTrait; + public function __construct(protected AppKernelConfig $appKernelConfig) { } @@ -50,6 +53,11 @@ public function validate(GlueRequestTransfer $glueRequestTransfer): GlueRequestV try { $validator->validate($psr7Request); } catch (Throwable $throwable) { + $this->getLogger()->error( + $this->getMessageFromThrowable($throwable), + $glueRequestTransfer->getMeta(), + ); + $glueErrorTransfer = new GlueErrorTransfer(); $glueErrorTransfer ->setMessage($this->getMessageFromThrowable($throwable));