Skip to content

Commit dcc4733

Browse files
authored
Merge pull request from GHSA-vr2x-7687-h6qv
1 parent 47e9d55 commit dcc4733

File tree

8 files changed

+67
-62
lines changed

8 files changed

+67
-62
lines changed

features/authorization/deny.feature

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,12 @@ Feature: Authorization checking
210210
Then the response status code should be 200
211211
And the response should contain "ownerOnlyProperty"
212212
And the JSON node "ownerOnlyProperty" should be equal to the string "updated"
213+
214+
Scenario: A user retrieves a resource with an admin only viewable property
215+
When I add "Accept" header equal to "application/json"
216+
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
217+
And I send a "GET" request to "/secured_dummies"
218+
Then the response status code should be 200
219+
And the response should contain "ownerOnlyProperty"
220+
221+

src/Hal/Serializer/CollectionNormalizer.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ protected function getPaginationData(iterable $object, array $context = []): arr
4242
[$paginator, $paginated, $currentPage, $itemsPerPage, $lastPage, $pageTotalItems, $totalItems] = $this->getPaginationConfig($object, $context);
4343
$parsed = IriHelper::parseIri($context['uri'] ?? '/', $this->pageParameterName);
4444

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

4948
$data = [

src/Hydra/Serializer/CollectionNormalizer.php

Lines changed: 25 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -19,68 +19,68 @@
1919
use ApiPlatform\JsonLd\ContextBuilderInterface;
2020
use ApiPlatform\JsonLd\Serializer\JsonLdContextTrait;
2121
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
22-
use ApiPlatform\Serializer\ContextTrait;
22+
use ApiPlatform\Serializer\AbstractCollectionNormalizer;
2323
use ApiPlatform\State\Pagination\PaginatorInterface;
2424
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
25-
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
26-
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
27-
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
28-
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
29-
use Symfony\Component\Serializer\Serializer;
3025

3126
/**
3227
* This normalizer handles collections.
3328
*
3429
* @author Kevin Dunglas <[email protected]>
3530
* @author Samuel ROZE <[email protected]>
3631
*/
37-
final class CollectionNormalizer implements NormalizerInterface, NormalizerAwareInterface, CacheableSupportsMethodInterface
32+
final class CollectionNormalizer extends AbstractCollectionNormalizer
3833
{
39-
use ContextTrait;
4034
use JsonLdContextTrait;
41-
use NormalizerAwareTrait;
4235

4336
public const FORMAT = 'jsonld';
4437
public const IRI_ONLY = 'iri_only';
4538
private array $defaultContext = [
4639
self::IRI_ONLY => false,
4740
];
4841

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

5346
if ($this->resourceMetadataCollectionFactory) {
5447
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));
5548
}
56-
}
5749

58-
/**
59-
* {@inheritdoc}
60-
*/
61-
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
62-
{
63-
return self::FORMAT === $format && is_iterable($data);
50+
parent::__construct($resourceClassResolver, '');
6451
}
6552

6653
/**
67-
* {@inheritdoc}
68-
*
69-
* @param iterable $object
54+
* Gets the pagination data.
7055
*/
71-
public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
56+
protected function getPaginationData(iterable $object, array $context = []): array
7257
{
73-
if (!isset($context['resource_class']) || isset($context['api_sub_level'])) {
74-
return $this->normalizeRawCollection($object, $format, $context);
75-
}
76-
7758
$resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class']);
7859
$context = $this->initContext($resourceClass, $context);
7960
$context['api_collection_sub_level'] = true;
8061
$data = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context);
8162
$data['@id'] = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context);
8263
$data['@type'] = 'hydra:Collection';
64+
65+
if ($object instanceof PaginatorInterface) {
66+
$data['hydra:totalItems'] = $object->getTotalItems();
67+
}
68+
69+
if (\is_array($object) || ($object instanceof \Countable && !$object instanceof PartialPaginatorInterface)) {
70+
$data['hydra:totalItems'] = \count($object);
71+
}
72+
73+
return $data;
74+
}
75+
76+
/**
77+
* Gets items data.
78+
*/
79+
protected function getItemsData(iterable $object, string $format = null, array $context = []): array
80+
{
81+
$data = [];
8382
$data['hydra:member'] = [];
83+
8484
$iriOnly = $context[self::IRI_ONLY] ?? $this->defaultContext[self::IRI_ONLY];
8585

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

111-
if ($object instanceof PaginatorInterface) {
112-
$data['hydra:totalItems'] = $object->getTotalItems();
113-
}
114-
115-
if (\is_array($object) || ($object instanceof \Countable && !$object instanceof PartialPaginatorInterface)) {
116-
$data['hydra:totalItems'] = \count($object);
117-
}
118-
119-
return $data;
120-
}
121-
122-
public function hasCacheableSupportsMethod(): bool
123-
{
124-
return true;
125-
}
126-
127-
/**
128-
* Normalizes a raw collection (not API resources).
129-
*/
130-
protected function normalizeRawCollection(iterable $object, string $format = null, array $context = []): array|\ArrayObject
131-
{
132-
if (\is_array($object) && !$object && ($context[Serializer::EMPTY_ARRAY_AS_OBJECT] ?? false)) {
133-
return new \ArrayObject();
134-
}
135-
136-
$data = [];
137-
foreach ($object as $index => $obj) {
138-
$data[$index] = $this->normalizer->normalize($obj, $format, $context);
139-
}
140-
141111
return $data;
142112
}
143113
}

src/JsonApi/Serializer/CollectionNormalizer.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

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

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

5249
$data = [

src/Serializer/AbstractCollectionNormalizer.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\Serializer;
1515

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

9091
unset($context['operation']);
9192
unset($context['operation_type'], $context['operation_name']);
93+
9294
$itemsData = $this->getItemsData($object, $format, $context);
9395

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

142+
protected function getOperation(array $context = []): Operation
143+
{
144+
$metadata = $this->resourceMetadataFactory->create($context['resource_class'] ?? '');
145+
146+
return $metadata->getOperation($context['operation_name'] ?? null);
147+
}
148+
140149
/**
141150
* Gets the pagination data.
142151
*/

src/Serializer/AbstractItemNormalizer.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use ApiPlatform\Exception\InvalidArgumentException;
2020
use ApiPlatform\Exception\ItemNotFoundException;
2121
use ApiPlatform\Metadata\ApiProperty;
22+
use ApiPlatform\Metadata\CollectionOperationInterface;
2223
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2324
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2425
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
@@ -114,6 +115,11 @@ public function normalize(mixed $object, string $format = null, array $context =
114115
return $this->serializer->normalize($object, $format, $context);
115116
}
116117

118+
if (isset($context['operation']) && $context['operation'] instanceof CollectionOperationInterface) {
119+
unset($context['operation']);
120+
unset($context['iri']);
121+
}
122+
117123
if ($this->resourceClassResolver->isResourceClass($resourceClass)) {
118124
$context = $this->initContext($resourceClass, $context);
119125
}

src/Serializer/CacheKeyTrait.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313

1414
namespace ApiPlatform\Serializer;
1515

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

2633
try {
27-
return md5($format.serialize($context));
34+
return hash('xxh128', $format.serialize([
35+
'context' => $context,
36+
'ignored' => $context[self::IGNORED_ATTRIBUTES] ?? $this->defaultContext[self::IGNORED_ATTRIBUTES],
37+
]));
2838
} catch (\Exception) {
2939
// The context cannot be serialized, skip the cache
3040
return false;

src/Serializer/SerializerContextBuilder.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use ApiPlatform\Util\RequestAttributesExtractor;
1919
use Symfony\Component\HttpFoundation\Request;
2020
use Symfony\Component\Serializer\Encoder\CsvEncoder;
21+
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
2122

2223
/**
2324
* {@inheritdoc}
@@ -77,6 +78,10 @@ public function createFromRequest(Request $request, bool $normalization, array $
7778
}
7879
}
7980

81+
// to keep the cache computation smaller, we have "operation_name" and "iri" anyways
82+
$context[AbstractObjectNormalizer::EXCLUDE_FROM_CACHE_KEY][] = 'root_operation';
83+
$context[AbstractObjectNormalizer::EXCLUDE_FROM_CACHE_KEY][] = 'operation';
84+
8085
return $context;
8186
}
8287
}

0 commit comments

Comments
 (0)