diff --git a/core/modules/jsonapi/src/Normalizer/HttpExceptionNormalizer.php b/core/modules/jsonapi/src/Normalizer/HttpExceptionNormalizer.php index 5062f11a696efe052983514f800e99f443c1bcdc..7737ce00cf82e49bee2a76f4f5199c853b7c2ab1 100644 --- a/core/modules/jsonapi/src/Normalizer/HttpExceptionNormalizer.php +++ b/core/modules/jsonapi/src/Normalizer/HttpExceptionNormalizer.php @@ -49,7 +49,9 @@ public function __construct(AccountInterface $current_user) { * {@inheritdoc} */ public function normalize($object, $format = NULL, array $context = []) { - return new HttpExceptionNormalizerValue(new CacheableMetadata(), static::rasterizeValueRecursive($this->buildErrorObjects($object))); + $cacheability = new CacheableMetadata(); + $cacheability->addCacheableDependency($object); + return new HttpExceptionNormalizerValue($cacheability, static::rasterizeValueRecursive($this->buildErrorObjects($object))); } /** diff --git a/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php b/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php index 9961720018a0b5072d74ba673c69570a7e7183cd..43fffa436fcfa244577706a38e9cf696a5c0de65 100644 --- a/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php +++ b/core/modules/jsonapi/tests/src/Functional/JsonApiRegressionTest.php @@ -1131,4 +1131,60 @@ public function testEmptyMapFieldTypeDenormalization() { $this->assertSame($doc['data']['attributes']['data'], Json::decode((string) $response->getBody())['data']['attributes']['data']); } + /** + * Ensure EntityAccessDeniedHttpException cacheability is taken into account. + */ + public function testLeakCacheMetadataInOmitted() { + $term = Term::create([ + 'name' => 'Llama term', + 'vid' => 'tags', + ]); + $term->setUnpublished(); + $term->save(); + + $node = Node::create([ + 'type' => 'article', + 'title' => 'Llama node', + 'field_tags' => ['target_id' => $term->id()], + ]); + $node->save(); + + $user = $this->drupalCreateUser([ + 'access content', + ]); + $request_options = [ + RequestOptions::AUTH => [ + $user->getAccountName(), + $user->pass_raw, + ], + ]; + + // Request with unpublished term. At this point it would include the term + // into "omitted" part of the response. The point here is that we + // purposefully warm up the cache where it is excluded from response and + // on the next run we will assure merely publishing term is enough to make + // it visible, i.e. that the 1st response was invalidated in Drupal cache. + $url = Url::fromUri('internal:/jsonapi/' . $node->getEntityTypeId() . '/' . $node->bundle(), [ + 'query' => ['include' => 'field_tags'], + ]); + $response = $this->request('GET', $url, $request_options); + $this->assertSame(200, $response->getStatusCode()); + + $response = Json::decode((string) $response->getBody()); + $this->assertArrayNotHasKey('included', $response, 'JSON API response does not contain "included" taxonomy term as the latter is not published, i.e not accessible.'); + + $omitted = $response['meta']['omitted']['links']; + unset($omitted['help']); + $omitted = reset($omitted); + $expected_url = Url::fromUri('internal:/jsonapi/' . $term->getEntityTypeId() . '/' . $term->bundle() . '/' . $term->uuid()); + $expected_url->setAbsolute(); + $this->assertSame($expected_url->toString(), $omitted['href'], 'Entity that is excluded due to access constraints is correctly reported in the "Omitted" section of the JSON API response.'); + + $term->setPublished(); + $term->save(); + $response = $this->request('GET', $url, $request_options); + $this->assertSame(200, $response->getStatusCode()); + $this->assertEquals($term->uuid(), Json::decode((string) $response->getBody())['included'][0]['id'], 'JSON API response contains "included" taxonomy term as it became published, i.e accessible.'); + } + }