Skip to content

Commit 7e737d5

Browse files
committed
Merge 4.2
2 parents e45b579 + c7ababf commit 7e737d5

File tree

7 files changed

+184
-16
lines changed

7 files changed

+184
-16
lines changed

.github/workflows/release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ jobs:
7070
steps:
7171
- name: Generate App Token
7272
id: generate_token
73-
uses: actions/create-github-app-token@v1
73+
uses: tibdex/github-app-token@v2
7474
with:
75-
app-id: ${{ secrets.API_PLATFORM_APP_ID }}
76-
private-key: ${{ secrets.API_PLATFORM_APP_PRIVATE_KEY }}
75+
app_id: ${{ secrets.API_PLATFORM_APP_ID }}
76+
private_key: ${{ secrets.API_PLATFORM_APP_PRIVATE_KEY }}
7777

7878
- name: Update demo
7979
env:

src/Hydra/Serializer/DocumentationNormalizer.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,10 @@ private function populateEntrypointProperties(ApiResource $resourceMetadata, str
109109
'domain' => '#Entrypoint',
110110
'owl:maxCardinality' => 1,
111111
'range' => [
112-
['@id' => $hydraPrefix.'Collection'],
112+
['@id' => 'hydra:Collection'],
113113
[
114114
'owl:equivalentClass' => [
115-
'owl:onProperty' => ['@id' => $hydraPrefix.'member'],
115+
'owl:onProperty' => ['@id' => 'hydra:member'],
116116
'owl:allValuesFrom' => ['@id' => $prefixedShortName],
117117
],
118118
],

src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -899,10 +899,10 @@ public function testNormalizeWithoutPrefix(): void
899899
'@type' => 'Link',
900900
'domain' => '#Entrypoint',
901901
'range' => [
902-
['@id' => 'Collection'],
902+
['@id' => 'hydra:Collection'],
903903
[
904904
'owl:equivalentClass' => [
905-
'owl:onProperty' => ['@id' => 'member'],
905+
'owl:onProperty' => ['@id' => 'hydra:member'],
906906
'owl:allValuesFrom' => ['@id' => '#dummy'],
907907
],
908908
],

src/Serializer/AbstractItemNormalizer.php

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use ApiPlatform\Metadata\Exception\AccessDeniedException;
1919
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
2020
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
21+
use ApiPlatform\Metadata\Exception\PropertyNotFoundException;
2122
use ApiPlatform\Metadata\IriConverterInterface;
2223
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2324
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
@@ -276,10 +277,6 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a
276277
$previousObject = $this->clone($objectToPopulate);
277278
$object = parent::denormalize($data, $type, $format, $context);
278279

279-
if (!$this->resourceClassResolver->isResourceClass($type)) {
280-
return $object;
281-
}
282-
283280
// Bypass the post-denormalize attribute revert logic if the object could not be
284281
// cloned since we cannot possibly revert any changes made to it.
285282
if (null !== $objectToPopulate && null === $previousObject) {
@@ -296,7 +293,13 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a
296293
// Revert attributes that aren't allowed to be changed after a post-denormalize check
297294
foreach (array_keys($data) as $attribute) {
298295
$attribute = $this->nameConverter ? $this->nameConverter->denormalize((string) $attribute) : $attribute;
299-
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $attribute, $options);
296+
297+
try {
298+
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $attribute, $options);
299+
} catch (PropertyNotFoundException) {
300+
continue;
301+
}
302+
300303
$attributeExtraProperties = $propertyMetadata->getExtraProperties();
301304
$throwOnPropertyAccessDenied = $attributeExtraProperties['throw_on_access_denied'] ?? $throwOnAccessDenied;
302305
if (!\in_array($attribute, $propertyNames, true)) {
@@ -500,12 +503,13 @@ protected function isAllowedAttribute(object|string $classOrObject, string $attr
500503
*/
501504
protected function canAccessAttribute(?object $object, string $attribute, array $context = []): bool
502505
{
503-
if (!$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
506+
$options = $this->getFactoryOptions($context);
507+
508+
try {
509+
$propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
510+
} catch (PropertyNotFoundException) {
504511
return true;
505512
}
506-
507-
$options = $this->getFactoryOptions($context);
508-
$propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
509513
$security = $propertyMetadata->getSecurity() ?? $propertyMetadata->getPolicy();
510514
if (null !== $this->resourceAccessChecker && $security) {
511515
return $this->resourceAccessChecker->isGranted($context['resource_class'], $security, [
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\Get;
18+
use ApiPlatform\Metadata\Operation;
19+
use ApiPlatform\Metadata\Patch;
20+
use ApiPlatform\Tests\Fixtures\TestBundle\Dto\SecuredInputDto;
21+
22+
#[ApiResource(
23+
operations: [
24+
new Get(provider: [self::class, 'provide']),
25+
new Patch(input: SecuredInputDto::class, processor: [self::class, 'process'], provider: [self::class, 'provide']),
26+
]
27+
)]
28+
class DummyDtoSecuredInput
29+
{
30+
public ?int $id = null;
31+
32+
public ?string $title = null;
33+
34+
public ?string $adminOnly = null;
35+
36+
public function getId(): ?int
37+
{
38+
return $this->id;
39+
}
40+
41+
public static function provide(Operation $operation, array $uriVariables = [], array $context = []): self
42+
{
43+
$entity = new self();
44+
$entity->id = (int) $uriVariables['id'];
45+
$entity->title = 'existing title';
46+
$entity->adminOnly = 'existing admin value';
47+
48+
return $entity;
49+
}
50+
51+
public static function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): self
52+
{
53+
if (!$data instanceof SecuredInputDto) {
54+
throw new \InvalidArgumentException('Expected SecuredInputDto');
55+
}
56+
57+
$entity = $context['previous_data'] ?? new self();
58+
if (null !== $data->title) {
59+
$entity->title = $data->title;
60+
}
61+
if (null !== $data->adminOnly) {
62+
$entity->adminOnly = $data->adminOnly;
63+
}
64+
65+
return $entity;
66+
}
67+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Dto;
15+
16+
use ApiPlatform\Metadata\ApiProperty;
17+
18+
class SecuredInputDto
19+
{
20+
public ?string $title = null;
21+
22+
#[ApiProperty(security: "is_granted('ROLE_ADMIN')")]
23+
public ?string $adminOnly = null;
24+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Functional;
15+
16+
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\DummyDtoSecuredInput;
18+
use ApiPlatform\Tests\SetupClassResourcesTrait;
19+
use Symfony\Component\Security\Core\User\InMemoryUser;
20+
21+
final class SecurityPropertyInputDtoTest extends ApiTestCase
22+
{
23+
use SetupClassResourcesTrait;
24+
25+
protected static ?bool $alwaysBootKernel = false;
26+
27+
/**
28+
* @return class-string[]
29+
*/
30+
public static function getResources(): array
31+
{
32+
return [DummyDtoSecuredInput::class];
33+
}
34+
35+
public function testNonAdminCannotWriteSecuredProperty(): void
36+
{
37+
$client = self::createClient();
38+
$client->loginUser(new InMemoryUser('user', 'password', ['ROLE_USER']));
39+
40+
$response = $client->request('PATCH', '/dummy_dto_secured_inputs/1', [
41+
'headers' => ['Content-Type' => 'application/merge-patch+json'],
42+
'json' => [
43+
'title' => 'updated title',
44+
'adminOnly' => 'hacked value',
45+
],
46+
]);
47+
48+
$this->assertResponseIsSuccessful();
49+
$json = $response->toArray();
50+
$this->assertSame('updated title', $json['title']);
51+
// The adminOnly field should be silently dropped (not written) for non-admin
52+
$this->assertSame('existing admin value', $json['adminOnly']);
53+
}
54+
55+
public function testAdminCanWriteSecuredProperty(): void
56+
{
57+
$client = self::createClient();
58+
$client->loginUser(new InMemoryUser('admin', 'password', ['ROLE_ADMIN']));
59+
60+
$response = $client->request('PATCH', '/dummy_dto_secured_inputs/1', [
61+
'headers' => ['Content-Type' => 'application/merge-patch+json'],
62+
'json' => [
63+
'title' => 'admin updated',
64+
'adminOnly' => 'admin value',
65+
],
66+
]);
67+
68+
$this->assertResponseIsSuccessful();
69+
$json = $response->toArray();
70+
$this->assertSame('admin updated', $json['title']);
71+
$this->assertSame('admin value', $json['adminOnly']);
72+
}
73+
}

0 commit comments

Comments
 (0)