Skip to content

Commit

Permalink
Feature/acp 4074/acp 4403 invalid checkout with paypalexpress in glue…
Browse files Browse the repository at this point in the history
… does not produce helpful errors (#19)

* ACP-4403 Added Request validation against OpenApi schema.
  • Loading branch information
stereomon authored Nov 26, 2024
1 parent 85304d7 commit b145dd9
Show file tree
Hide file tree
Showing 12 changed files with 572 additions and 9 deletions.
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ jobs:
fail-fast: false
matrix:
php-version: [
'8.1',
'8.2',
]
steps:
Expand Down
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"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",
"spryker/glue-application-extension": "^1.0.0",
"spryker/kernel": "^3.30.0",
Expand Down
22 changes: 18 additions & 4 deletions resources/api/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,24 @@ info:
name: Spryker
url: 'https://spryker.com/app-composition-platform/'
email: [email protected]
title: 'Hello World'
title: 'AppKernel API'
license:
name: MIT
url: 'https://opensource.org/licenses/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':
post:
operationId: 'configure'
summary: 'Saves or updates Hello World App configuration between Tenants and this App.'
security:
- Bearer: [ ]
parameters:
- $ref: '#/components/parameters/tenantIdentifier'
requestBody:
Expand Down Expand Up @@ -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:
Expand All @@ -69,6 +76,13 @@ paths:
schema:
type: string
components:
securitySchemes:
Bearer:
type: http
scheme: bearer
bearerFormat: JWT
description: >-
Enter the token with the `Bearer ` prefix, e.g. "Bearer abcde12345".
schemas:
ConfigurationApiRequest:
properties:
Expand Down
8 changes: 8 additions & 0 deletions src/Spryker/Glue/AppKernel/AppKernelConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace Spryker\Glue\AppKernel;

use Spryker\Glue\Kernel\AbstractBundleConfig;
use Spryker\Shared\AppKernel\AppKernelConstants;

class AppKernelConfig extends AbstractBundleConfig
{
Expand Down Expand Up @@ -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;
}
}
9 changes: 9 additions & 0 deletions src/Spryker/Glue/AppKernel/AppKernelFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

/**
* Copyright © 2016-present Spryker Systems GmbH. All rights reserved.
* Use of this software requires acceptance of the Evaluation License Agreement. See LICENSE file.
*/

namespace Spryker\Glue\AppKernel\Plugin\GlueApplication;

use Generated\Shared\Transfer\GlueRequestTransfer;
use Generated\Shared\Transfer\GlueRequestValidationTransfer;
use Spryker\Glue\GlueApplicationExtension\Dependency\Plugin\RequestValidatorPluginInterface;
use Spryker\Glue\Kernel\AbstractPlugin;

/**
* @method \Spryker\Glue\AppKernel\AppKernelFactory getFactory()
*/
class AppGlueRequestSchemaValidatorPlugin extends AbstractPlugin implements RequestValidatorPluginInterface
{
public function validate(GlueRequestTransfer $glueRequestTransfer): GlueRequestValidationTransfer
{
return $this->getFactory()->createOpenApiRequestSchemaValidator()->validate($glueRequestTransfer);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

/**
* Copyright © 2016-present Spryker Systems GmbH. All rights reserved.
* Use of this software requires acceptance of the Evaluation License Agreement. See LICENSE file.
*/

namespace Spryker\Glue\AppKernel\Validator;

use Generated\Shared\Transfer\GlueErrorTransfer;
use Generated\Shared\Transfer\GlueRequestTransfer;
use Generated\Shared\Transfer\GlueRequestValidationTransfer;
use GuzzleHttp\Psr7\Request;
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)
{
}

public function validate(GlueRequestTransfer $glueRequestTransfer): GlueRequestValidationTransfer
{
$glueRequestValidationTransfer = new GlueRequestValidationTransfer();
$glueRequestValidationTransfer->setIsValid(true);

$openApiSchemaPath = $this->appKernelConfig->getOpenApiSchemaPath();

if ($openApiSchemaPath === null || $openApiSchemaPath === '' || $openApiSchemaPath === '0') {
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) {
$this->getLogger()->error(
$this->getMessageFromThrowable($throwable),
$glueRequestTransfer->getMeta(),
);

$glueErrorTransfer = new GlueErrorTransfer();
$glueErrorTransfer
->setMessage($this->getMessageFromThrowable($throwable));

$glueRequestValidationTransfer
->setIsValid(false)
->addError($glueErrorTransfer)
->setStatus(Response::HTTP_BAD_REQUEST);

return $glueRequestValidationTransfer;
}

return $glueRequestValidationTransfer;
}

protected function getMessageFromThrowable(Throwable $throwable): string
{
return match (get_class($throwable)) {
InvalidBody::class => $throwable->getVerboseMessage(),
default => $throwable->getMessage(),
};
}
}
10 changes: 10 additions & 0 deletions src/Spryker/Shared/AppKernel/AppKernelConstants.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Loading

0 comments on commit b145dd9

Please sign in to comment.