Skip to content

Commit

Permalink
Merge pull request from GHSA-vr2x-7687-h6qv
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka authored Feb 27, 2023
1 parent 47e9d55 commit dcc4733
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 62 deletions.
9 changes: 9 additions & 0 deletions features/authorization/deny.feature
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,12 @@ Feature: Authorization checking
Then the response status code should be 200
And the response should contain "ownerOnlyProperty"
And the JSON node "ownerOnlyProperty" should be equal to the string "updated"

Scenario: A user retrieves a resource with an admin only viewable property
When I add "Accept" header equal to "application/json"
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
And I send a "GET" request to "/secured_dummies"
Then the response status code should be 200
And the response should contain "ownerOnlyProperty"


3 changes: 1 addition & 2 deletions src/Hal/Serializer/CollectionNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ protected function getPaginationData(iterable $object, array $context = []): arr
[$paginator, $paginated, $currentPage, $itemsPerPage, $lastPage, $pageTotalItems, $totalItems] = $this->getPaginationConfig($object, $context);
$parsed = IriHelper::parseIri($context['uri'] ?? '/', $this->pageParameterName);

$metadata = $this->resourceMetadataFactory->create($context['resource_class'] ?? '');
$operation = $metadata->getOperation($context['operation_name'] ?? null);
$operation = $context['operation'] ?? $this->getOperation($context);
$urlGenerationStrategy = $operation->getUrlGenerationStrategy();

$data = [
Expand Down
80 changes: 25 additions & 55 deletions src/Hydra/Serializer/CollectionNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,68 +19,68 @@
use ApiPlatform\JsonLd\ContextBuilderInterface;
use ApiPlatform\JsonLd\Serializer\JsonLdContextTrait;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Serializer\ContextTrait;
use ApiPlatform\Serializer\AbstractCollectionNormalizer;
use ApiPlatform\State\Pagination\PaginatorInterface;
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Serializer;

/**
* This normalizer handles collections.
*
* @author Kevin Dunglas <dunglas@gmail.com>
* @author Samuel ROZE <samuel.roze@gmail.com>
*/
final class CollectionNormalizer implements NormalizerInterface, NormalizerAwareInterface, CacheableSupportsMethodInterface
final class CollectionNormalizer extends AbstractCollectionNormalizer
{
use ContextTrait;
use JsonLdContextTrait;
use NormalizerAwareTrait;

public const FORMAT = 'jsonld';
public const IRI_ONLY = 'iri_only';
private array $defaultContext = [
self::IRI_ONLY => false,
];

public function __construct(private readonly ContextBuilderInterface $contextBuilder, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly IriConverterInterface $iriConverter, private readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, array $defaultContext = [])
public function __construct(private readonly ContextBuilderInterface $contextBuilder, ResourceClassResolverInterface $resourceClassResolver, private readonly IriConverterInterface $iriConverter, private readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, array $defaultContext = [])
{
$this->defaultContext = array_merge($this->defaultContext, $defaultContext);

if ($this->resourceMetadataCollectionFactory) {
trigger_deprecation('api-platform/core', '3.0', sprintf('Injecting "%s" within "%s" is not needed anymore and this dependency will be removed in 4.0.', ResourceMetadataCollectionFactoryInterface::class, self::class));
}
}

/**
* {@inheritdoc}
*/
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
{
return self::FORMAT === $format && is_iterable($data);
parent::__construct($resourceClassResolver, '');
}

/**
* {@inheritdoc}
*
* @param iterable $object
* Gets the pagination data.
*/
public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
protected function getPaginationData(iterable $object, array $context = []): array
{
if (!isset($context['resource_class']) || isset($context['api_sub_level'])) {
return $this->normalizeRawCollection($object, $format, $context);
}

$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class']);
$context = $this->initContext($resourceClass, $context);
$context['api_collection_sub_level'] = true;
$data = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context);
$data['@id'] = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context);
$data['@type'] = 'hydra:Collection';

if ($object instanceof PaginatorInterface) {
$data['hydra:totalItems'] = $object->getTotalItems();
}

if (\is_array($object) || ($object instanceof \Countable && !$object instanceof PartialPaginatorInterface)) {
$data['hydra:totalItems'] = \count($object);
}

return $data;
}

/**
* Gets items data.
*/
protected function getItemsData(iterable $object, string $format = null, array $context = []): array
{
$data = [];
$data['hydra:member'] = [];

$iriOnly = $context[self::IRI_ONLY] ?? $this->defaultContext[self::IRI_ONLY];

if (($operation = $context['operation'] ?? null) && method_exists($operation, 'getItemUriTemplate')) {
Expand Down Expand Up @@ -108,36 +108,6 @@ public function normalize(mixed $object, string $format = null, array $context =
}
}

if ($object instanceof PaginatorInterface) {
$data['hydra:totalItems'] = $object->getTotalItems();
}

if (\is_array($object) || ($object instanceof \Countable && !$object instanceof PartialPaginatorInterface)) {
$data['hydra:totalItems'] = \count($object);
}

return $data;
}

public function hasCacheableSupportsMethod(): bool
{
return true;
}

/**
* Normalizes a raw collection (not API resources).
*/
protected function normalizeRawCollection(iterable $object, string $format = null, array $context = []): array|\ArrayObject
{
if (\is_array($object) && !$object && ($context[Serializer::EMPTY_ARRAY_AS_OBJECT] ?? false)) {
return new \ArrayObject();
}

$data = [];
foreach ($object as $index => $obj) {
$data[$index] = $this->normalizer->normalize($obj, $format, $context);
}

return $data;
}
}
5 changes: 1 addition & 4 deletions src/JsonApi/Serializer/CollectionNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

use ApiPlatform\Api\ResourceClassResolverInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\Serializer\AbstractCollectionNormalizer;
use ApiPlatform\Util\IriHelper;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
Expand Down Expand Up @@ -44,9 +43,7 @@ protected function getPaginationData($object, array $context = []): array
[$paginator, $paginated, $currentPage, $itemsPerPage, $lastPage, $pageTotalItems, $totalItems] = $this->getPaginationConfig($object, $context);
$parsed = IriHelper::parseIri($context['uri'] ?? '/', $this->pageParameterName);

/** @var ResourceMetadataCollection */
$metadata = $this->resourceMetadataFactory->create($context['resource_class'] ?? '');
$operation = $metadata->getOperation($context['operation_name'] ?? null);
$operation = $context['operation'] ?? $this->getOperation($context);
$urlGenerationStrategy = $operation->getUrlGenerationStrategy();

$data = [
Expand Down
9 changes: 9 additions & 0 deletions src/Serializer/AbstractCollectionNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Serializer;

use ApiPlatform\Api\ResourceClassResolverInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\State\Pagination\PaginatorInterface;
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
Expand Down Expand Up @@ -89,6 +90,7 @@ public function normalize(mixed $object, string $format = null, array $context =

unset($context['operation']);
unset($context['operation_type'], $context['operation_name']);

$itemsData = $this->getItemsData($object, $format, $context);

return array_merge_recursive($data, $paginationData, $itemsData);
Expand Down Expand Up @@ -137,6 +139,13 @@ protected function getPaginationConfig(iterable $object, array $context = []): a
return [$paginator, $paginated, $currentPage, $itemsPerPage, $lastPage, $pageTotalItems, $totalItems];
}

protected function getOperation(array $context = []): Operation
{
$metadata = $this->resourceMetadataFactory->create($context['resource_class'] ?? '');

return $metadata->getOperation($context['operation_name'] ?? null);
}

/**
* Gets the pagination data.
*/
Expand Down
6 changes: 6 additions & 0 deletions src/Serializer/AbstractItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use ApiPlatform\Exception\InvalidArgumentException;
use ApiPlatform\Exception\ItemNotFoundException;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
Expand Down Expand Up @@ -114,6 +115,11 @@ public function normalize(mixed $object, string $format = null, array $context =
return $this->serializer->normalize($object, $format, $context);
}

if (isset($context['operation']) && $context['operation'] instanceof CollectionOperationInterface) {
unset($context['operation']);
unset($context['iri']);
}

if ($this->resourceClassResolver->isResourceClass($resourceClass)) {
$context = $this->initContext($resourceClass, $context);
}
Expand Down
12 changes: 11 additions & 1 deletion src/Serializer/CacheKeyTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@

namespace ApiPlatform\Serializer;

/**
* Used to override Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::getCacheKey which is private
* We need the cache_key in JsonApi and Hal before it is computed in Symfony.
*
* @see https://github.com/symfony/symfony/blob/49b6ab853d81e941736a1af67845efa3401e7278/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php#L723 which isn't protected
*/
trait CacheKeyTrait
{
private function getCacheKey(?string $format, array $context): string|bool
Expand All @@ -21,10 +27,14 @@ private function getCacheKey(?string $format, array $context): string|bool
unset($context[$key]);
}
unset($context[self::EXCLUDE_FROM_CACHE_KEY]);
unset($context[self::OBJECT_TO_POPULATE]);
unset($context['cache_key']); // avoid artificially different keys

try {
return md5($format.serialize($context));
return hash('xxh128', $format.serialize([
'context' => $context,
'ignored' => $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES],
]));
} catch (\Exception) {
// The context cannot be serialized, skip the cache
return false;
Expand Down
5 changes: 5 additions & 0 deletions src/Serializer/SerializerContextBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use ApiPlatform\Util\RequestAttributesExtractor;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\Encoder\CsvEncoder;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;

/**
* {@inheritdoc}
Expand Down Expand Up @@ -77,6 +78,10 @@ public function createFromRequest(Request $request, bool $normalization, array $
}
}

// to keep the cache computation smaller, we have "operation_name" and "iri" anyways
$context[AbstractObjectNormalizer::EXCLUDE_FROM_CACHE_KEY][] = 'root_operation';
$context[AbstractObjectNormalizer::EXCLUDE_FROM_CACHE_KEY][] = 'operation';

return $context;
}
}

0 comments on commit dcc4733

Please sign in to comment.