Discovered while working on #2853066: Spec Compliance: Inaccessible collection/related resources surface errors: should be 200 with hypermedia + metadata.

When requesting a related route like: /jsonapi/node/article/{uuid}/field_related, access should be dependent on the parent entity and field access result. Not any of the access results for the related entities.

IOW, the related route needs to be treated just like a collection... since it's just a collection that is filtered using "referenced by" semantics.

CommentFileSizeAuthor
#63 2985426-63.patch21.46 KBwim leers
#63 interdiff.txt641 byteswim leers
#62 2985426-62.patch21.82 KBwim leers
#62 interdiff.txt1.74 KBwim leers
#60 interdiff-56-60.txt2.3 KBgabesullice
#60 2985426-60.patch21.78 KBgabesullice
#56 2985426-56.patch22.5 KBgabesullice
#47 interdiff-44-47.txt795 bytesgabesullice
#47 2985426-47.patch22.88 KBgabesullice
#47 2985426-47-COMBINED.patch42.61 KBgabesullice
#44 interdiff-42-combined.patch19.23 KBgabesullice
#44 2985426-44.COMBINED.patch42.55 KBgabesullice
#44 2985426-44.patch22.25 KBgabesullice
#42 2985426-42.patch38.99 KBgabesullice
#42 interdiff-35-42.txt1.38 KBgabesullice
#36 interdiff-rename-entity-collection-36.txt15.67 KBgabesullice
#35 interdiff-34-rename-entity-collection.txt15.14 KBgabesullice
#35 2985426-34.patch37.84 KBgabesullice
#35 interdiff-32-34.txt7.89 KBgabesullice
#32 interdiff-rename.txt15.02 KBgabesullice
#32 interdiff-29-32.txt14.67 KBgabesullice
#32 2985426-32.patch46.29 KBgabesullice
#29 2985426-29.patch32.88 KBgabesullice
#29 interdiff-27-29.txt4.73 KBgabesullice
#27 2985426-27.patch31.23 KBgabesullice
#27 interdiff-26-27.txt3.7 KBgabesullice
#26 2985426-26.patch28.66 KBgabesullice
#26 interdiff-23-26.txt3.57 KBgabesullice
#24 2985426-23.patch27.91 KBgabesullice
#23 interdiff-21-23.txt2.87 KBgabesullice
#21 2985426-21.patch25.77 KBgabesullice
#21 interdiff-19-21.txt3.12 KBgabesullice
#19 2985426-19.patch25.06 KBgabesullice
#19 interdiff-17-19.txt7.52 KBgabesullice
#17 2985426-17.patch20.8 KBgabesullice
#17 interdiff-12-17.txt1.67 KBgabesullice
#12 2985426-12.patch18.78 KBgabesullice
#12 interdiff-8-12.txt2.17 KBgabesullice
#8 2985426-8.patch16.61 KBgabesullice
#8 interdiff-7-8.txt1.61 KBgabesullice
#7 2985426-7.patch15 KBgabesullice
#7 interdiff-6-7.txt8.71 KBgabesullice
#6 2985426-6.patch6.29 KBgabesullice
#6 interdiff-5-6.txt4.57 KBgabesullice
#2 2985426-2.patch1.97 KBgabesullice

Comments

gabesullice created an issue. See original summary.

gabesullice’s picture

StatusFileSize
new1.97 KB

This is the most basic change of assumptions. I'm not sure that we have any scenarios in the tests where we have an accessible field but only inaccessible targeted resource though, so this might still pass.

gabesullice’s picture

Assigned: Unassigned » gabesullice
Status: Active » Needs review

Status: Needs review » Needs work

The last submitted patch, 2: 2985426-2.patch, failed testing. View results

gabesullice’s picture

1) Drupal\Tests\jsonapi\Functional\TermTest::testRelated
Failed asserting that 403 is identical to 200.

Awesome, so we do have a failing case already :)

Did some digging and this is caused by a term's vid relationship (the relationship to the terms vocabulary) and since we're running the test without a permission for viewing that config entity, we get the failure we want (for now).

gabesullice’s picture

StatusFileSize
new4.57 KB
new6.29 KB

Further refining the expectations. Since #2940339: Port reference field support for non-empty entity reference fields not pointing to an entity from #2543726, if we treat related routes like collection routes, we'll need to take virtual resource identifiers into consideration.

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new8.71 KB
new15 KB

No sense in just *acting* like this is a collection, let's use the getCollection code.

This should work pretty much as expected multiple cardinality fields. But we'll need to do some tweaking for single cardinality fields. Remember, an empty collection is always [], but a single value relationship that's empty is NULL, so that's going to need to be communicated somehow if we're gonna reuse that code.

gabesullice’s picture

StatusFileSize
new1.61 KB
new16.61 KB

Remember, an empty collection is always [], but a single value relationship that's empty is NULL, so that's going to need to be communicated somehow if we're gonna reuse that code.

I forgot to include this in my last patch.

The last submitted patch, 7: 2985426-7.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

The last submitted patch, 6: 2985426-6.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 8: 2985426-8.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new2.17 KB
new18.78 KB

Okay, the meat of the changes to handle single-value collections in the normalizers. Let's see what's left.

Status: Needs review » Needs work

The last submitted patch, 12: 2985426-12.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

When requesting a related route like: /jsonapi/node/article/{uuid}/field_related, access should be dependent on the parent entity and field access result. Not any of the access results for the related entities.

IOW, the related route needs to be treated just like a collection... since it's just a collection that is filtered using "referenced by" semantics.

I don't fully grok this.

Can you explain this using a concrete example?

gabesullice’s picture

Sure!

Consider that you make an entity reference field on a node to nodes. That node can have multiple references. Let's call it field_related_articles. You create articles A, B and C. You publish A and B.

Now, from A, you add a reference to B and C.

You request /jsonapi/node/article/A/field_related, the data you receive will be [B] because C is omitted because it's unpublished. If you then requested /jsonapi/node/article you'd get [A, B]. If you filtered out A, then the first response would be entirely indistinguishable from the second response if not for the self links. The related route is a resource collection.

Now, the spec goes on to say:

null is only an appropriate response when the requested URL is one that might correspond to a single resource, but doesn’t currently.

Note: Consider, for example, a request to fetch a to-one related resource link. This request would respond with null when the relationship is empty (such that the link is corresponding to no resources) but with the single related resource’s resource object otherwise.

In #2940339: Port reference field support for non-empty entity reference fields not pointing to an entity from #2543726, we established that the relationship route can list relationship objects that are not represented at the related route. If a term's parent were single value and referencing a "virtual" resource, what would it return? It should return null, because it's "an appropriate response when the requested URL is one that might correspond to a single resource, but doesn’t currently."

Long story short, if we have a single value related route, we should return null, because everywhere else in the API, we omit inaccessible resources. We don't return 403, even when all items have been omitted.

getRelated doesn't work like that though, it proxies the request for a single value related field to getIndividual, which then returns a 403, because the code thinks it's dealing with a direct request for that individual resource.

Just like the collections route, the related route access should not be dependent on what resources are represented there. Regardless if its for a to-one or a to-many relationship.

wim leers’s picture

Thanks, that helps a lot 👌

This bit from the IS now makes perfect sense, and is a perfect summary:

IOW, the related route needs to be treated just like a collection... since it's just a collection that is filtered using "referenced by" semantics.

Thanks, and please proceed!

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new1.67 KB
new20.8 KB

Our include tests became invalid after this change because they use expected related responses to generate the expected results. Since this moves individual forbidden access errors from the top-level errors member to the meta.errors, it wasn't picking them up correctly.

This will still be failing because some cache info isn't yet handled.

Status: Needs review » Needs work

The last submitted patch, 17: 2985426-17.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

gabesullice’s picture

StatusFileSize
new7.52 KB
new25.06 KB

This makes everything that we put into EntityCollection cacheable.

wim leers’s picture

+++ b/src/LabelOnlyEntity.php
@@ -19,6 +24,7 @@ class LabelOnlyEntity {
+    $this->setCacheability(CacheableMetadata::createFromObject($entity));

This can just be $this->setCacheability($entity);

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new3.12 KB
new25.77 KB

Status: Needs review » Needs work

The last submitted patch, 21: 2985426-21.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new2.87 KB

Make sure that only the primary collection query is filtered by bundle. IOW, don't add a bundle filter to the related collection.

Cleaning up and addressing reviews next :)

gabesullice’s picture

StatusFileSize
new27.91 KB

Status: Needs review » Needs work

The last submitted patch, 24: 2985426-23.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

gabesullice’s picture

StatusFileSize
new3.57 KB
new28.66 KB

Code standards and #20 addressed.

Now, last remaining failure. Then self-review and un-assign :)

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new3.7 KB
new31.23 KB

Should be green 🤞

gabesullice’s picture

Self-review. Next patch to contain changes.


  1. +++ b/src/Controller/EntityResource.php
    @@ -341,9 +342,46 @@ class EntityResource {
    +   * Executes a query, checks access and returns an loaded entity collection.
    

    s/an/a

  2. +++ b/src/Controller/EntityResource.php
    @@ -341,9 +342,46 @@ class EntityResource {
    +   *   the query execution. For example, when node access applies to the query.
    

    "This happens when node access applies to the query for example."

  3. +++ b/src/Controller/EntityResource.php
    @@ -383,28 +421,10 @@ class EntityResource {
    -    $entity_collection = new EntityCollection(array_column($collection_data, 'entity'));
    +    $entity_collection = new EntityCollection(array_values($collection_data));
    

    Is array_values() necessary?

  4. +++ b/src/Controller/EntityResource.php
    @@ -423,46 +443,32 @@ class EntityResource {
    -    if (!$is_multiple && $field_list->entity) {
    -      $response = $this->getIndividual($field_list->entity, $request);
    -      // Add cacheable metadata for host entity to individual response.
    -      $response->addCacheableDependency($cacheable_metadata);
    -      return $response;
    -    }
    

    This is the "big change" we're no longer saying a single-value related route is the same and getIndividual. Everything else is fallout from this.

  5. +++ b/src/Controller/EntityResource.php
    @@ -423,46 +443,32 @@ class EntityResource {
    +    $route_params = $request->attributes->get('_route_params');
    +    $params = isset($route_params['_json_api_params']) ? $route_params['_json_api_params'] : [];
    ...
    +      $query = $this->getCollectionQuery($entity_type_id, $params);
    

    This makes related routes pageable and filterable!... BUT, we shouldn't introduce a big feature like that without tests. I'm going to comment this out and make a followup. I'm in no hurry to have that done.

  6. +++ b/src/Controller/EntityResource.php
    @@ -423,46 +443,32 @@ class EntityResource {
    +    // Add the cacheable metadata from the host entity.
    +    $response->addCacheableDependency($entity);
    

    The entity list cache tag is not added to this one, that's because the list will only change when the host entity changes.

  7. +++ b/src/Controller/EntityResource.php
    @@ -423,46 +443,32 @@ class EntityResource {
    +    $response->addCacheableDependency($query_cacheability);
    

    Copy same comment from above: "Ensure that any cacheability from query execution is applied."

  8. +++ b/src/Controller/EntityResource.php
    @@ -765,14 +770,6 @@ class EntityResource {
    -    // Limit this query to the bundle type for this resource.
    -    $bundle = $this->resourceType->getBundle();
    -    if ($bundle && ($bundle_key = $entity_type->getKey('bundle'))) {
    -      $query->condition(
    -        $bundle_key, $bundle
    -      );
    -    }
    
    @@ -789,7 +786,28 @@ class EntityResource {
    +  /**
    +   * Adds a bundle condition to an entity query for a given resource type.
    ...
    +   */
    +  protected function filterQueryByBundle(ResourceType $resource_type, QueryInterface $query) {
    

    This was moved so that it could be applied separately from the related collection query.

  9. +++ b/src/Controller/EntityResource.php
    @@ -822,12 +840,9 @@ class EntityResource {
    -    // When a new change to any entity in the resource happens, we cannot ensure
    -    // the validity of this cached list. Add the list tag to deal with that.
    -    $list_tag = $this->entityTypeManager->getDefinition($entity_type_id)
    -      ->getListCacheTags();
    -    $response->getCacheableMetadata()->addCacheTags($list_tag);
    

    This was moved so that it would apply only to the getCollection query.

  10. +++ b/src/Controller/EntityResource.php
    @@ -939,37 +954,29 @@ class EntityResource {
    -   * @return array
    -   *   An array containing the keys:
    -   *     - entity: the loaded entity or an access exception.
    -   *     - access: the access object.
    -   *
    -   * @throws \Drupal\jsonapi\Exception\EntityAccessDeniedHttpException
    -   *   Thrown when the current user is not allowed to GET the selected resource.
    +   * @return \Drupal\Core\Entity\EntityInterface|\Drupal\jsonapi\LabelOnlyEntity|\Drupal\jsonapi\Exception\EntityAccessDeniedHttpException
    +   *   The loaded entity, a label only version of that entity or an
    +   *   EntityAccessDeniedHttpException object if neither is accessible.
    

    We no longer need to return an entity/access tuple because LabelOnlyEntity and EntityAccessDeniedHttpException now carry their own cacheability.

  11. +++ b/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php
    @@ -201,7 +211,7 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface,
    -      if ($rasterized_include['data'] === FALSE) {
    +      if (empty($rasterized_include['data'])) {
    

    This was a really subtle bug to track down. It only appeared when two or more includes were forbidden and would cause all but one include to go missing!

  12. +++ b/tests/src/Functional/JsonApiFunctionalTest.php
    @@ -94,7 +94,7 @@ class JsonApiFunctionalTest extends JsonApiFunctionalTestBase {
         // 8b. Single related item, empty.
    ...
    -    $this->assertSame([], $single_output['data']);
    +    $this->assertSame(NULL, $single_output['data']);
    

    This is the change.

  13. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1688,28 +1688,24 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +          // Remove an relationships to 'virtual' resources.
    

    s/an/any

gabesullice’s picture

Title: `related` routes should return 200, not 403, if field access is allowed but the related resources are forbidden » Spec compliance: `related` routes should return 200, not 403, if field access is allowed but the related resources are forbidden
Assigned: gabesullice » Unassigned
StatusFileSize
new4.73 KB
new32.88 KB

1. Done.
2. Made it better, I think.
3. I ended up fixing the comments for loadEntitiesWithAccess() to reflect that it no longer returns an entity/access tuple and I moved the array_values call to it.
...
5. Created #2986169: [PP-1] Support sortable, paginated and filterable related resources..
...
7. Done.
13. Done.


Ready for a final review! 🎉

wim leers’s picture

Status: Needs review » Needs work

#28:

Thanks especially for points 4, 5 and 6!

Point 11 sounds like an absolute nightmare to track down! Well done! 👏


This patch is mostly just moving existing code around. Logic in EntityResource::getCollection() and EntityResource::respondWithCollection() is being moved around to allow for code reuse by EntityResource::getRelated().

Makes sense.

  1. +++ b/src/Controller/EntityResource.php
    @@ -212,9 +213,9 @@ class EntityResource {
    +      // by the user. Field access makes no difference between 'create' and
    

    s/difference/distinction/ if we're changing these lines anyway.

  2. +++ b/src/Controller/EntityResource.php
    @@ -423,46 +444,35 @@ class EntityResource {
    -    $referenced_entities = array_filter(
    -      $field_list->referencedEntities(),
    ...
    +    $target_entity_ids = array_map(function (EntityInterface $target_entity) {
    +      return $target_entity->id();
    +    }, $field_list->filterEmptyItems()->referencedEntities());
    +
    +    $query_cacheability = new CacheableMetadata();
    +    if (!empty($target_entity_ids)) {
    +      $query = $this->getCollectionQuery($entity_type_id, $params);
    +      $query->condition($this->entityTypeManager->getDefinition($entity_type_id)->getKey('id'), $target_entity_ids, 'IN');
    

    We went from using ::referencedEntities() to still using that, but using it to perform a query.

    We didn't do that query before.

    I'm not sure how to feel about that. Can you explain your rationale?

  3. +++ b/src/Exception/EntityAccessDeniedHttpException.php
    @@ -13,8 +16,9 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
    -class EntityAccessDeniedHttpException extends HttpException {
    +class EntityAccessDeniedHttpException extends HttpException implements CacheableDependencyInterface {
    

    Then let's extends \Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException instead of extends HttpException implements CacheableDependencyInterface. That's what #2929428: [>=8.5] Convert "throw new *HttpException" into "throw new Cacheable*HttpException" where possible is about, but this is then at least already one tiny step :)

  4. +++ b/src/Exception/EntityAccessDeniedHttpException.php
    @@ -51,6 +55,7 @@ class EntityAccessDeniedHttpException extends HttpException {
    +    $this->setCacheability(CacheableMetadata::createFromObject($entity_access));
    

    Same remark as in #20.

  5. +++ b/src/JsonApiResource/EntityCollection.php
    @@ -21,6 +21,13 @@ class EntityCollection implements \IteratorAggregate, \Countable {
    +   * Whether this collection can have more than one resource.
    

    Let's document when/why this would not be the case, because "collection" pretty much implies "can contain multiple" for any reader.

  6. +++ b/src/JsonApiResource/EntityCollection.php
    @@ -40,15 +47,20 @@ class EntityCollection implements \IteratorAggregate, \Countable {
    +    assert($is_multiple || count($entities) <= 1);
    

    👍

  7. +++ b/src/JsonApiResource/EntityCollection.php
    @@ -118,4 +130,14 @@ class EntityCollection implements \IteratorAggregate, \Countable {
    +   * Gets the cardinality of this collection.
    ...
    +  public function isMultiple() {
    

    "cardinality" vs "multiple".

    I like "cardinality" better.

  8. +++ b/src/LabelOnlyEntity.php
    @@ -9,7 +11,9 @@ use Drupal\Core\Entity\EntityInterface;
    -class LabelOnlyEntity {
    +class LabelOnlyEntity implements CacheableDependencyInterface {
    

    👍

  9. +++ b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
    @@ -203,7 +203,7 @@ class JsonApiDocumentTopLevelNormalizer extends NormalizerBase implements Denorm
    -    return new JsonApiDocumentTopLevelNormalizerValue($normalizer_values, $context, $link_context, $is_collection);
    +    return new JsonApiDocumentTopLevelNormalizerValue($normalizer_values, $context, $link_context, $is_collection && $data->isMultiple());
    

    This is confusing. We're telling this constructor that the values do not represent a collection if it's a single-cardinality collection.

    So really, at that point, EntityCollection is more like an EntityContainer?

  10. +++ b/src/Normalizer/RelationshipItemNormalizer.php
    @@ -59,8 +59,8 @@ class RelationshipItemNormalizer extends FieldItemNormalizer {
    -      $entity_and_access = EntityResource::getEntityAndAccess($target_entity);
    ...
    +      $entity = EntityResource::getEntityAndAccess($target_entity);
    

    It's confusing that the variable name was changed, but the method name remains the same.

  11. +++ b/tests/src/Functional/ResourceResponseTestTrait.php
    @@ -500,4 +475,27 @@ trait ResourceResponseTestTrait {
    +  /**
    +   * Gets a generic empty collection response.
    +   *
    +   * @return \Drupal\jsonapi\ResourceResponse
    +   *   The empty collection ResourceResponse.
    +   */
    +  protected static function getEmptyCollectionResponse($is_multiple, $self_link) {
    

    Nit: incomplete docblock.

  12. +++ b/tests/src/Kernel/Controller/EntityResourceTest.php
    @@ -238,7 +238,11 @@ class EntityResourceTest extends JsonapiKernelTestBase {
    -    $this->assertEquals(['config:node_type_list'], $response->getCacheableMetadata()->getCacheTags());
    +    $expected_cache_tags = [
    +      'config:node.type.article',
    +      'config:node_type_list',
    +    ];
    +    $this->assertSame($expected_cache_tags, $response->getCacheableMetadata()->getCacheTags());
    
    @@ -275,7 +279,12 @@ class EntityResourceTest extends JsonapiKernelTestBase {
    -    $this->assertEquals(['config:node_type_list'], $response->getCacheableMetadata()->getCacheTags());
    +    $expected_cache_tags = [
    +      'config:node.type.article',
    +      'config:node.type.lorem',
    +      'config:node_type_list',
    +    ];
    +    $this->assertSame($expected_cache_tags, $response->getCacheableMetadata()->getCacheTags());
    

    Oh, these were missing before? Nice catch! 👏

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new46.29 KB
new14.67 KB
new15.02 KB

1. ✅
2. I realized the same thing near the end. We totally could just pass the results of referencedEntities() into the EntityCollection constructor (after checking access ofc). That's mostly because it wasn't until the end that I decided that we shouldn't introduce filtering/sorting in this issue. So, what's the rationale at this point? It queues up #2986169: [PP-1] Support sortable, paginated and filterable related resources. as a four line change + tests:

+++ b/src/Controller/EntityResource.php
@@ -423,46 +444,35 @@ class EntityResource {
+    // @todo: remove and uncomment following two lines in https://www.drupal.org/project/jsonapi/issues/2986169.
+    $params = [];
+    /* $route_params = $request->attributes->get('_route_params'); */
+    /* $params = isset($route_params['_json_api_params']) ? $route_params['_json_api_params'] : []; */

3. Good point. ✔️
4. $entity_access must only implement AccessResultInterfce, but setCacheability() requires the argument to implement CacheableDependencyInterface.
5. I went ahead and aligned all the naming and docs to be in line the concepts/language of the spec, e.g. EntityCollection -> ResourceCollection as in "related resource collection" (Cmd + F "resource collection" reveals numerous references on http://jsonapi.org/format/). The rename interdiff is separate from everything else.
7. ✅
8. 👍
9. I hope this isn't confusing anymore because of #5. LMK if you still find it confusing.
10. Good point. Changed to getAccessCheckedEntity(). Does that work?
11. ✅
12. 👍


#31 Huh, didn't remember that that even existed. I tried to verify that this fixed it (because it would have) but it turns out that it's already working for related routes. I updated that issue title to reflect that it only applies to relationship routes, which this does not fix.

wim leers’s picture

  • 2. I hate to say this but … I think it'd be much easier to understand this issue and the next if those query changes happened in the next issue.
  • 4. Ahhh, good point, I missed that! 👍
  • 5. Hmmmm … that sort of feels out of scope for this issue, but I guess it's silly to do that in a separate issue too. I do worry about how this impacts JSON API Extras. So I checked. Zero mentions of EntityCollection … meaning no impact! Then I can live with this change happening here.
  • 10. Yes!

Interdiff review:

  1. +++ b/src/Controller/EntityResource.php
    @@ -444,12 +444,11 @@ class EntityResource {
    -    $is_multiple = $field_list->getDataDefinition()->getFieldStorageDefinition()->isMultiple();
    
    @@ -458,13 +457,15 @@ class EntityResource {
    +      $resource_collection = new ResourceCollection($this->doCollectionQuery($query, $query_cacheability)->toArray(), $field_storage_definition->getCardinality());
    

    Oh, hah, Drupal core apparently also confuses "multiple" and "cardinality" :)

  2. +++ b/src/JsonApiResource/ResourceCollection.php
    @@ -22,11 +22,11 @@ class ResourceCollection implements \IteratorAggregate, \Countable {
    -   * Whether this collection can have more than one resource.
    +   * The number of resources permitted in this collection.
        *
        * @var bool
        */
    -  protected $isMultiple;
    +  protected $cardinality;
    

    s/bool/int/

  3. +++ b/src/JsonApiResource/ResourceCollection.php
    @@ -43,24 +43,26 @@ class ResourceCollection implements \IteratorAggregate, \Countable {
    -   * Instantiates a EntityCollection object.
    +   * Instantiates a ResourceCollection object.
    ...
    +   * @param \Drupal\Core\Entity\EntityInterface|null[] $resources
    +   *   The resources for the collection.
    

    "resource collection" but it contains "entity" objects.

    I don't think it's worth renaming this class right now.

  4. +++ b/src/JsonApiResource/ResourceCollection.php
    @@ -43,24 +43,26 @@ class ResourceCollection implements \IteratorAggregate, \Countable {
    +  public function __construct(array $resources, $cardinality = -1) {
    
    +++ b/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php
    @@ -69,10 +69,13 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface,
    +  public function __construct(array $values, array $context, array $link_context, $cardinality = 1) {
    

    Why different defaults?

    Or, why even have defaults at all? Why not require explicitness instead?

  5. +++ b/src/JsonApiResource/ResourceCollection.php
    @@ -43,24 +43,26 @@ class ResourceCollection implements \IteratorAggregate, \Countable {
    +    assert($cardinality < 0 || count($resources) <= $cardinality);
    

    Shouldn't we also assert that the cardinality is non-zero *and* not smaller than minus 1?
    Only valid values are: -1, 1, 2, 3 … N

wim leers’s picture

Status: Needs review » Needs work
gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new7.89 KB
new37.84 KB
new15.14 KB

I hate to say this but … I think it'd be much easier to understand this issue and the next if those query changes happened in the next issue.

Don't worry about it :) You're right.


1. Heh, yep.
2. ✅
3.

"resource collection" but it contains "entity" objects.

Sort of. It contains entities, but also our own LabelOnlyEntity wrapper, and now EntityAccessDeniedHttpExceptions too. At some point, I hope it will contain what will end up being resource identifier objects.

I'm confused:

I do worry about how this impacts JSON API Extras. So I checked. Zero mentions of EntityCollection … meaning no impact! Then I can live with this change happening here."
vs.
I don't think it's worth renaming this class right now.

I didn't include the rename in the patch, but I did include an interdiff that will apply to the patch if you want to include it on commit.

I'll admit, this has been a personal pet peeve of mine for a while... now seemed like an opportune time to get it done.

4. I bet you didn't realize this would be the biggest time sink of that review but it was! :P Turns out the RelationshipNormalizerValue has its own cardinality logic. So when JsonApiDocumentTopLevelNormalizerValue is given any value other than 1, it doubly wraps the array of values in a multiple cardinality field. I didn't try to resolve that conflict in this patch, it ended up opening up a can of worms. I did make it a required argument though.

5. ✅

gabesullice’s picture

StatusFileSize
new15.67 KB

I forgot one property in the rename patch.

The last submitted patch, 32: 2985426-32.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 35: 2985426-34.patch, failed testing. View results

gabesullice’s picture

Status: Needs work » Needs review

This was kicked back because HEAD broke on the latest version of core.

The last submitted patch, 26: 2985426-26.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 35: 2985426-34.patch, failed testing. View results

gabesullice’s picture

StatusFileSize
new1.38 KB
new38.99 KB

The fail in #35 is actually a bad assertion. 401 is the correct response code when the user is not authenticated and 403 is correct when the user is authenticated but not authorized to view a particular entity.

gabesullice’s picture

Title: Spec compliance: `related` routes should return 200, not 403, if field access is allowed but the related resources are forbidden » [PP-1] Spec compliance: `related` routes should return 200, not 403, if field access is allowed but the related resources are forbidden
Assigned: Unassigned » gabesullice
Status: Needs work » Active

This is just getting too big, making it super difficult to review.

I broke off an issue for the EntityAccessDeniedHttpException piece: #2986987: Convert EntityAccessDeniedHttpException into cacheable exception

Taking for myself to reroll a smaller patch.

gabesullice’s picture

Status: Active » Needs review
StatusFileSize
new22.25 KB
new42.55 KB
new19.23 KB

Alright, here's the patch without that issue included (it is a blocker though).

Combined patch included as well.

Interdiff is between the last patch and the combined patch. What's in the interdiff? Getting rid of all the moving around that happened in order to make the query work, but that was removed in #35. Also, getting rid of the no longer necessary $request argument and response code arguments in the various EntityResource methods.

The last submitted patch, 44: 2985426-44.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 44: interdiff-42-combined.patch, failed testing. View results

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new42.61 KB
new22.88 KB
new795 bytes

Status: Needs review » Needs work

The last submitted patch, 47: 2985426-47.patch, failed testing. View results

gabesullice’s picture

Status: Needs work » Needs review

Combined patch passed.

wim leers’s picture

Status: Needs review » Postponed
  • #35.3: I probably wrote that comment in two sittings. I did check impact on JSON API Extras and from that POV I can live with this change. But I think renaming it is also very much out of scope here. Such a rename merits its own separate issue. Also: I totally missed that interdiff-rename wasn't part of the patch yet 😳 Sorry!
  • #35.4: I indeed did not anticipate that! 😮 I'm fine with not fixing a separate can of worms here (because: scope creep). Thanks for making cardinality a required argument. Explicitness FTW.
  • #42: I've fixed the same problem in core REST's test coverage a long time ago 👍
  • #43: 🙏 Thanks for making this patch more reviewable.
  • #44: Waiting to review until the blocker is in. Note that I asked for that blocker (#2986987) to be more tightly scoped itself. It's generally better to have a larger number of issues but that are each more tightly scoped. Then each individual patch can be committed much quicker, because it's far easier to understand :)

Marking postponed.

gabesullice’s picture

Title: [PP-1] Spec compliance: `related` routes should return 200, not 403, if field access is allowed but the related resources are forbidden » [PP-2] Spec compliance: `related` routes should return 200, not 403, if field access is allowed but the related resources are forbidden
Related issues: +#2987206: Refactor `getEntityAndAccess` to return cacheable objects with access cacheability rather than an entity/access pair.
wim leers’s picture

Title: [PP-2] Spec compliance: `related` routes should return 200, not 403, if field access is allowed but the related resources are forbidden » [PP-1] Spec compliance: `related` routes should return 200, not 403, if field access is allowed but the related resources are forbidden
wim leers’s picture

Title: [PP-1] Spec compliance: `related` routes should return 200, not 403, if field access is allowed but the related resources are forbidden » Spec compliance: `related` routes should return 200, not 403, if field access is allowed but the related resources are forbidden
Status: Postponed » Needs review

Status: Needs review » Needs work

The last submitted patch, 47: 2985426-47.patch, failed testing. View results

wim leers’s picture

#47 doesn't apply because I asked in #2986987-11: Convert EntityAccessDeniedHttpException into cacheable exception.2 to descope it, and you did. I think we can do this patch without those changes.

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new22.5 KB

Rerolled for the reasons described in #55.

gabesullice’s picture

Assigned: gabesullice » Unassigned
wim leers’s picture

Status: Needs review » Needs work

Sorry for the long silence — I was on vacation since the 24th!

This is now basically down to nits and making the set of changes as minimal as possible, to make it simpler for future readers to understand this change:

  1. +++ b/src/Controller/EntityResource.php
    @@ -401,51 +402,33 @@ class EntityResource {
    -    $referenced_entities = array_filter(
    -      $field_list->referencedEntities(),
    -      function (EntityInterface $entity) {
    -        return (bool) $this->resourceTypeRepository->get(
    -          $entity->getEntityTypeId(),
    -          $entity->bundle()
    -        );
    -      }
    -    );
    ...
    +    $referenced_entities = array_filter($field_list->filterEmptyItems()->referencedEntities(), function (EntityInterface $entity) {
    +      return (bool) $this->resourceTypeRepository->get($entity->getEntityTypeId(), $entity->bundle());
    +    });
    

    This is 99% just reformatting code.

    The only actual change here is ->filterEmptyItems(). Is this a necessary change? If so, do we have test coverage for it?

    If not essential, should we defer this to a separate minor bugfix issue?

  2. +++ b/src/Controller/EntityResource.php
    @@ -401,51 +402,33 @@ class EntityResource {
    -    foreach ($referenced_entities as $referenced_entity) {
    -      $collection_data[$referenced_entity->id()] = static::getAccessCheckedEntity($referenced_entity);
    ...
    -    $entity_collection = new EntityCollection($collection_data);
    ...
    +    // Check access to each of the referenced entities.
    +    $access_checked_entities = array_map(function (EntityInterface $entity) {
    +      return static::getAccessCheckedEntity($entity);
    +    }, $referenced_entities);
    +
    +    $entity_collection = new EntityCollection($access_checked_entities, $field_list->getFieldDefinition()->getFieldStorageDefinition()->getCardinality());
    

    This again seems to be 100% identical. It'd be simpler to understand and hence commit this if these changes were omitted.

  3. +++ b/src/Controller/EntityResource.php
    @@ -401,51 +402,33 @@ class EntityResource {
    -      $cacheable_metadata->addCacheableDependency($referenced_entity);
    ...
    -    $response->addCacheableDependency($cacheable_metadata);
    +    $response->addCacheableDependency($entity);
    
    +++ b/tests/src/Kernel/Controller/EntityResourceTest.php
    @@ -393,11 +391,7 @@ class EntityResourceTest extends JsonapiKernelTestBase {
    -      ['node:1', 'node:2', 'node:3', 'node:4'],
    ...
    +    $this->assertEquals(['node:4'], $response->getCacheableMetadata()->getCacheTags());
    

    This is a bugfix 👍

  4. +++ b/src/JsonApiResource/EntityCollection.php
    @@ -36,19 +43,27 @@ class EntityCollection implements \IteratorAggregate, \Countable {
    -   * Instantiates a EntityCollection object.
    +   * Instantiates a ResourceCollection object.
    

    Nit: Leftover that needs to be reverted.

wim leers’s picture

Note: I fully expect to be able to commit this after just one more reroll! 🙏👌

gabesullice’s picture

StatusFileSize
new21.78 KB
new2.3 KB

1 + 2: Done. I seem to remember filterEmptyItems() being a necessary change because of a call ->getEntityTypeId() on NULL error, but the two most likely test classes (CommentTest and TermTest) both pass locally, so we'll see if it's really necessary after testbot runs. Hopefully not :)
3. Aye.
4. Good aye.

gabesullice’s picture

Status: Needs work » Needs review
wim leers’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new1.74 KB
new21.82 KB

MUCH better! Because much less change :)

  1. +++ b/src/JsonApiResource/EntityCollection.php
    @@ -38,17 +45,25 @@ class EntityCollection implements \IteratorAggregate, \Countable {
    +   *   to-one relationship should have a cardinality of 1. Use -1 is for
    

    Übernit: s/is for/for/

  2. +++ b/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php
    @@ -147,7 +153,7 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface,
           // If this is a collection we need to append the pager data.
    

    This comment is outdated. Can just be removed.

Other than those two doc nitpicks, this is RTBC! I fixed those docs nitpicks myself. Committing… 🎉

wim leers’s picture

StatusFileSize
new641 bytes
new21.46 KB
+use Drupal\Core\Entity\Entity;

This is actually unused. Removing this.

(There is one other CS violation, but that one is pre-existing.)

  • Wim Leers committed 6305b2a on 8.x-2.x
    Issue #2985426 by gabesullice, Wim Leers: Spec compliance: `related`...
wim leers’s picture

Status: Reviewed & tested by the community » Fixed

Committed!

(I don't think @e0ipso would have wanted to give his blessing for this first, since this is a pure bugfix issue/patch, without significant architectural changes. The most significant changes are in the test coverage!)

wim leers’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.