Skip to content

Commit

Permalink
feat(laravel): parameter validator + security (#6603)
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka authored Sep 10, 2024
1 parent fa430c6 commit 53e654d
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 15 deletions.
12 changes: 11 additions & 1 deletion src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
use ApiPlatform\Laravel\Routing\SkolemIriConverter;
use ApiPlatform\Laravel\Security\ResourceAccessChecker;
use ApiPlatform\Laravel\State\AccessCheckerProvider;
use ApiPlatform\Laravel\State\ParameterValidatorProvider;
use ApiPlatform\Laravel\State\SwaggerUiProcessor;
use ApiPlatform\Laravel\State\SwaggerUiProvider;
use ApiPlatform\Laravel\State\ValidateProvider;
Expand Down Expand Up @@ -175,6 +176,7 @@
use ApiPlatform\State\Provider\DeserializeProvider;
use ApiPlatform\State\Provider\ParameterProvider;
use ApiPlatform\State\Provider\ReadProvider;
use ApiPlatform\State\Provider\SecurityParameterProvider;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\State\SerializerContextBuilderInterface;
use Illuminate\Config\Repository as ConfigRepository;
Expand Down Expand Up @@ -463,7 +465,15 @@ public function register(): void
$this->app->singleton(ParameterProvider::class, function (Application $app) {
$tagged = iterator_to_array($app->tagged(ParameterProviderInterface::class));

return new ParameterProvider($app->make(DeserializeProvider::class), new ServiceLocator($tagged));
return new ParameterProvider(
new ParameterValidatorProvider(
new SecurityParameterProvider(
$app->make(DeserializeProvider::class),
$app->make(ResourceAccessCheckerInterface::class)
),
),
new ServiceLocator($tagged)
);
});

$this->app->singleton(AccessCheckerProvider::class, function (Application $app) {
Expand Down
79 changes: 79 additions & 0 deletions src/Laravel/State/ParameterValidatorProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Laravel\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ParameterNotFound;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\State\Util\ParameterParserTrait;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Request;

/**
* Validates parameters using the Symfony validator.
*
* @implements ProviderInterface<object>
*
* @experimental
*/
final class ParameterValidatorProvider implements ProviderInterface
{
use ParameterParserTrait;
use ValidationErrorTrait;

/**
* @param ProviderInterface<object> $decorated
*/
public function __construct(
private readonly ?ProviderInterface $decorated = null,
) {
}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
if (!($request = $context['request']) instanceof Request) {
return $this->decorated->provide($operation, $uriVariables, $context);
}

$operation = $request->attributes->get('_api_operation') ?? $operation;
if (!($operation->getQueryParameterValidationEnabled() ?? true)) {
return $this->decorated->provide($operation, $uriVariables, $context);
}

$allConstraints = [];
foreach ($operation->getParameters() ?? [] as $parameter) {
if (!$constraints = $parameter->getConstraints()) {
continue;
}

$key = $parameter->getKey();
$value = $parameter->getValue();
if ($value instanceof ParameterNotFound) {
$value = null;
}

foreach ($constraints as $c) {
$allConstraints[] = $c;
}
}

$validator = Validator::make($request->query->all(), $allConstraints);
if ($validator->fails()) {
throw $this->getValidationError($validator, new ValidationException($validator));
}

return $this->decorated->provide($operation, $uriVariables, $context);
}
}
19 changes: 5 additions & 14 deletions src/Laravel/State/ValidateProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

namespace ApiPlatform\Laravel\State;

use ApiPlatform\Laravel\ApiResource\ValidationError;
use ApiPlatform\Metadata\Error;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
Expand All @@ -27,6 +26,8 @@
*/
final class ValidateProvider implements ProviderInterface
{
use ValidationErrorTrait;

/**
* @param ProviderInterface<object> $inner
*/
Expand Down Expand Up @@ -55,7 +56,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
$this->app->make($rules);
// } catch (AuthorizationException $e) { // TODO: we may want to catch this to transform to an error
} catch (ValidationException $e) { // @phpstan-ignore-line make->($rules) may throw this
throw $this->getValidationError($e);
throw $this->getValidationError($e->validator, $e);
}

return $body;
Expand All @@ -65,21 +66,11 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
return $body;
}

$validator = Validator::make($request->all(), $rules);
$validator = Validator::make($request->request->all(), $rules);
if ($validator->fails()) {
throw $this->getValidationError(new ValidationException($validator));
throw $this->getValidationError($validator, new ValidationException($validator));
}

return $body;
}

private function getValidationError(ValidationException $e): ValidationError
{
$violations = [];
foreach ($e->validator->errors()->messages() as $prop => $message) {
$violations[] = ['propertyPath' => $prop, 'message' => implode(\PHP_EOL, $message)];
}

return new ValidationError($e->getMessage(), spl_object_hash($e), $e, $violations);
}
}
33 changes: 33 additions & 0 deletions src/Laravel/State/ValidationErrorTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Laravel\State;

use ApiPlatform\Laravel\ApiResource\ValidationError;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException;

trait ValidationErrorTrait
{
private function getValidationError(Validator $validator, ValidationException $e): ValidationError
{
$errors = $validator->errors();
$violations = [];
$id = hash('xxh3', implode(',', $errors->keys()));
foreach ($errors->messages() as $prop => $message) {
$violations[] = ['propertyPath' => $prop, 'message' => implode(\PHP_EOL, $message)];
}

return new ValidationError($e->getMessage(), $id, $e, $violations);
}
}

0 comments on commit 53e654d

Please sign in to comment.