Skip to content

Commit 88262ab

Browse files
committed
Fix allowedComponents check blocking components loaded as Doctrine proxies
When PageData is loaded from DB, its component associations are lazy-loaded as Doctrine proxy instances. ComponentPositionNormalizer.normalizeForPageData() called \$component::class on the proxy, returning the proxy class name (Proxies\\__CG__\\...) rather than the real entity class. This caused getIriFromResource() to produce the wrong collection IRI, so the in_array() check against allowedComponents always failed — blocking all pageDataProperty components for anonymous users (any request that cold-loaded PageData from DB). Fix: detect Doctrine Proxy via instanceof and use get_parent_class() to recover the real class name, matching the pattern used in PropagateUpdatesListener. Adds Behat tests covering both the direct position fetch (path header) and the manifest endpoint, with manager->clear() to force proxy loading.
1 parent da702ce commit 88262ab

5 files changed

Lines changed: 63 additions & 2 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,4 +672,4 @@ A custom Symfony profiler panel showing how the bundle handled a request — sec
672672

673673
When a component has a file uploaded to S3 with private ACL, accessing the file requires a pre-signed temporary URL. The bundle's uploadable system doesn't currently handle the pre-signed URL lifecycle — the URL returned may be permanent and publicly accessible (or inaccessible).
674674

675-
**Fix direction:** The `Flysystem temporary URL` generator (`silverback.api_components.uploadable.url_generator.temporary`) likely generates pre-signed URLs already. Tests for this path with a real (or mock) S3 adapter are missing. Also: the download endpoint (`src/Action/Uploadable/DownloadAction.php`) can gate access — apps can hook into events — but this isn't documented or tested.
675+
**Fix direction:** The `Flysystem temporary URL` generator (`silverback.api_components.uploadable.url_generator.temporary`) likely generates pre-signed URLs already. Tests for this path with a real (or mock) S3 adapter are missing. Also: the download endpoint (`src/Action/Uploadable/DownloadAction.php`) can gate access — apps can hook into events — but this isn't documented or tested.

features/bootstrap/DoctrineContext.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,53 @@ public function thereIsAPageDataPropertyPositionWithDisallowedComponentType(stri
879879
$this->manager->flush();
880880
}
881881

882+
/**
883+
* @Given there is a pageDataProperty position with an allowed component type in a restricted group with route :path
884+
*/
885+
public function thereIsAPageDataPropertyPositionWithAllowedComponentType(string $path): void
886+
{
887+
$componentGroup = new ComponentGroup();
888+
$componentGroup->reference = 'test';
889+
$componentGroup->location = 'test';
890+
$componentGroup->allowedComponents = ['/component/dummy_components'];
891+
$this->timestampedHelper->persistTimestampedFields($componentGroup, true);
892+
$this->manager->persist($componentGroup);
893+
$this->restContext->resources['component_group'] = $this->iriConverter->getIriFromResource($componentGroup);
894+
895+
$componentPosition = new ComponentPosition();
896+
$componentPosition->pageDataProperty = 'component';
897+
$componentPosition->componentGroup = $componentGroup;
898+
$componentPosition->sortValue = 0;
899+
$this->timestampedHelper->persistTimestampedFields($componentPosition, true);
900+
$this->manager->persist($componentPosition);
901+
$this->restContext->resources['position_0'] = $this->iriConverter->getIriFromResource($componentPosition);
902+
903+
$page = new Page();
904+
$page->isTemplate = true;
905+
$page->reference = 'test page';
906+
$page->addComponentGroup($componentGroup);
907+
$this->timestampedHelper->persistTimestampedFields($page, true);
908+
$this->manager->persist($page);
909+
910+
$dummyComponent = $this->thereIsADummyComponent();
911+
912+
$pageData = new PageDataWithComponent();
913+
$pageData->component = $dummyComponent;
914+
$pageData->page = $page;
915+
$this->timestampedHelper->persistTimestampedFields($pageData, true);
916+
$this->manager->persist($pageData);
917+
$this->restContext->resources['page_data'] = $this->iriConverter->getIriFromResource($pageData);
918+
919+
$route = new Route();
920+
$route->setPath($path)->setName($path)->setPageData($pageData);
921+
$this->timestampedHelper->persistTimestampedFields($route, true);
922+
$this->manager->persist($route);
923+
$this->restContext->resources['page_data_route'] = $this->iriConverter->getIriFromResource($route);
924+
925+
$this->manager->flush();
926+
$this->manager->clear();
927+
}
928+
882929
/**
883930
* @Given there is a PageData resource with the route path :childPath nested within the route :parentPath
884931
*/

features/main/component_groups.feature

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ Feature: ComponentGroup resource
134134
Then the response status code should be 200
135135
And the JSON node "component" should be null
136136

137+
Scenario: An allowed pageDataProperty position in a restricted group returns the component for anonymous users
138+
Given there is a pageDataProperty position with an allowed component type in a restricted group with route "/test-page"
139+
And I add "path" header equal to "/test-page"
140+
When I send a "GET" request to the resource "position_0"
141+
Then the response status code should be 200
142+
And the JSON node "component" should match the regex "/\/component\/dummy_components\/[a-z0-9\-]+/"
143+
137144
@loginAdmin
138145
Scenario: Sending a PHP class name as allowedComponents is normalised to a collection IRI
139146
Given there is a ComponentGroup with 0 components

features/main/route.feature

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ Feature: Route resources
214214
Then the response status code should be 200
215215
And the JSON node "resource_iris[0][5]" should match the regex "/\/component\/dummy_components\/[a-z0-9\-]+/"
216216

217+
Scenario: The manifest includes component IRIs from allowedComponents-restricted pageDataProperty positions for anonymous users
218+
Given there is a pageDataProperty position with an allowed component type in a restricted group with route "/my-route"
219+
When I send a "GET" request to "/_/resource_manifest//my-route"
220+
Then the response status code should be 200
221+
And the JSON node "resource_iris[0][5]" should match the regex "/\/component\/dummy_components\/[a-z0-9\-]+/"
222+
217223
Scenario: The manifest for a nested PageData route includes parent resource IRIs grouped by depth
218224
Given there is a PageData resource with the route path "/conference/programme" nested within the route "/conference"
219225
When I send a "GET" request to "/_/resource_manifest//conference/programme"

src/Serializer/Normalizer/ComponentPositionNormalizer.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ApiPlatform\Metadata\UrlGeneratorInterface;
1717
use Doctrine\ORM\Mapping\ClassMetadata;
1818
use Doctrine\Persistence\ManagerRegistry;
19+
use Doctrine\Persistence\Proxy;
1920
use Silverback\ApiComponentsBundle\DataProvider\PageDataProvider;
2021
use Silverback\ApiComponentsBundle\Entity\Core\AbstractComponent;
2122
use Silverback\ApiComponentsBundle\Entity\Core\ComponentPosition;
@@ -194,7 +195,7 @@ private function normalizeForPageData(ComponentPosition $object, array $context)
194195

195196
// skip if the resolved component type is not in the group's allowedComponents
196197
if ($object->componentGroup && null !== $object->componentGroup->allowedComponents) {
197-
$resourceClass = $component::class;
198+
$resourceClass = $component instanceof Proxy ? get_parent_class($component) : $component::class;
198199
$iri = $this->iriConverter->getIriFromResource(
199200
$resourceClass,
200201
UrlGeneratorInterface::ABS_PATH,

0 commit comments

Comments
 (0)