Discovered as part of #2953321: Comprehensive JSON API integration test coverage phase 5: nested includes and sparse field sets.

If the request has an `include` for an inaccessible relationship, the only option is to throw an exception. However, our conventions around partial success suggest that this should simply be an error under meta.errors.

In the tests, I've had to add:

// @todo remove this conditional when inaccessible relationships are able to raise errors.
if ($error['detail'] !== 'The current user is not allowed to view this relationship.') {
  $expected_document['meta']['errors'][] = $error;
}

Todo:

  1. Decide if this should be a 400-level bad request, or if an error should be added to meta.errors.
  2. Implement the change.
CommentFileSizeAuthor
#100 2956084-100.patch26.87 KBgabesullice
#100 interdiff.txt438 bytesgabesullice
#99 interdiff.txt610 byteswim leers
#97 interdiff-code-standards.txt1.75 KBgabesullice
#97 interdiff.txt3.22 KBgabesullice
#97 2956084-97.patch25.68 KBgabesullice
#94 2956084-94.patch24.46 KBgabesullice
#94 interdiff.txt13.78 KBgabesullice
#93 2956084-92.patch11.48 KBgabesullice
#93 interdiff.txt1.76 KBgabesullice
#89 interdiff.txt940 bytesgabesullice
#89 2956084-89.patch10.72 KBgabesullice
#87 2956084-87.patch10.69 KBgabesullice
#87 interdiff.txt2.59 KBgabesullice
#85 2956084-85.patch8.53 KBgabesullice
#85 interdiff.txt5.19 KBgabesullice
#80 2956084-80.patch4.27 KBwim leers
#80 interdiff.txt972 byteswim leers
#77 2956084-77.patch4 KBgabesullice
#77 interdiff.txt1.29 KBgabesullice
#74 2956084-74.patch2.71 KBgabesullice
#70 2956084-70.patch36.44 KBwim leers
#66 2956084-66.patch35.45 KBgabesullice
#66 interdiff-63-66.txt6.03 KBgabesullice
#63 2956084-63.patch34.21 KBgabesullice
#63 interdiff-59-63.txt1.95 KBgabesullice
#62 2956084-61.patch34 KBgabesullice
#62 interdiff-59-61.txt1.33 KBgabesullice
#59 2956084-59.patch34 KBgabesullice
#59 interdiff-57-59.txt1.33 KBgabesullice
#57 2956084-57.patch32.88 KBgabesullice
#57 interdiff-56-57.txt2.67 KBgabesullice
#56 2956084-56.patch32.05 KBgabesullice
#56 interdiff-55-56.txt7.71 KBgabesullice
#55 2956084-55.patch27.74 KBgabesullice
#55 interdiff-54-55.txt2.24 KBgabesullice
#54 2956084-54.patch26.04 KBgabesullice
#50 2956084-50.patch26.89 KBwim leers
#50 interdiff.txt5.04 KBwim leers
#48 2956084-47.patch24.59 KBwim leers
#48 interdiff.txt6.42 KBwim leers
#46 2956084-46.patch19.47 KBwim leers
#46 interdiff.txt1.5 KBwim leers
#43 2956084-43.patch19.58 KBwim leers
#43 interdiff.txt2.05 KBwim leers
#41 2956084-41.patch18.05 KBwim leers
#41 interdiff.txt1.21 KBwim leers
#39 2956084-39.patch16.88 KBwim leers
#39 interdiff.txt3.14 KBwim leers
#37 2956084-37.patch15.42 KBwim leers
#37 interdiff.txt1.07 KBwim leers
#35 2956084-35.patch14.37 KBwim leers
#35 interdiff.txt524 byteswim leers
#31 2956084-31.patch13.87 KBwim leers
#31 interdiff.txt1.51 KBwim leers
#28 2956084-28.patch12.39 KBwim leers
#28 interdiff.txt2.7 KBwim leers
#26 2956084-26.patch10.33 KBwim leers
#26 interdiff.txt1.03 KBwim leers
#22 2956084-22.patch9.33 KBwim leers
#22 interdiff.txt6.04 KBwim leers
#20 2956084-20.patch5.11 KBwim leers
#18 2956084-19.patch5.3 KBwim leers
#18 interdiff.txt4.18 KBwim leers
#17 2956084-17.patch1.14 KBwim leers

Comments

gabesullice created an issue. See original summary.

gabesullice’s picture

@e0ipso/@Wim Leers, any thoughts about this? This has come back up as part of my testing of collections.

My preference would be to follow the same logic as I laid out here: #2853066-13: Spec Compliance: Inaccessible collection/related resources surface errors: should be 200 with hypermedia + metadata

e0ipso’s picture

Access can only be guaranteed at the entity level, the entity type definition is not necessarily enough. For me that's the last nudge into having each entity fail differently if necessary and succeed with the collection itself. So +1.

wim leers’s picture

+1

#2853066: Spec Compliance: Inaccessible collection/related resources surface errors: should be 200 with hypermedia + metadata was pushed to 2.x. Should this be pushed to 2.x too? Or can/should we do it in 1.x, because this is something that currently just is broken/fails? Can it be done elegantly in 1.x, or would it be much easier to just do it all in 2.x?

gabesullice’s picture

Title: Impossible to raise an error when an `include` is requested for an inaccessible relationship field. » [PP-1] Impossible to raise an error when an `include` is requested for an inaccessible relationship field.
Version: 8.x-1.x-dev » 8.x-2.x-dev
Status: Active » Postponed

I can't see a way to do this in 1.x without wasting a ton of effort.

I'm not sure "postponing" is the right term, I imagine this will be resolved in one go, but we should leave this here to be sure it happens.

gabesullice’s picture

wim leers’s picture

gabesullice’s picture

Sure.

This is my recollection of the issue, it's been a while since I was heads down in it:

EntityReferenceFieldNormalizer returns a Relationship object, which is normalized by RelationshipNormalizer (which also utilizes RelationshipItemNormalizer). RelationshipItemNormalizer then makes a call to JsonApiDocumentTopLevelNormalizer and passes that result when creating a RelationshipItemNormalizerValue. Later, in the actual top level normalizer value, a call to getInclude is made to all RelationshipItemNormalizerValues and that is put under the serialized include section.

Along that winding path, any include-related access denied exceptions get lost (or are unable to be passed through) to the final document.

wim leers’s picture

Thanks! (Admittedly I don't see exactly where these issues intersect, but I do see how they're related. I'm sure that #9 will help when we work on this issue!)

wim leers’s picture

  • gabesullice committed 93994bb on 8.x-1.x
    Issue #2940339 follow-up by gabesullice: restore test coverage exception...

  • gabesullice committed b0c0f15 on 8.x-2.x
    Issue #2940339 follow-up by gabesullice: restore test coverage exception...
gabesullice’s picture

The above two commits restore the test coverage exemptions in place until this issue is resolved. They do not solve this issue.

wim leers’s picture

Discovered as part of #2953321: Comprehensive JSON API integration test coverage phase 5: nested includes and sparse field sets.

Actually, this was discovered not in #2953321: Comprehensive JSON API integration test coverage phase 5: nested includes and sparse field sets, but in #2956084: Impossible to raise an error when an `include` is requested for an inaccessible relationship field..

At least, that's the issue that made the @todo start pointing to this issue. Because it was actually introduced in 165974 — for the 1.14 security release!

wim leers’s picture

Title: [PP-1] Impossible to raise an error when an `include` is requested for an inaccessible relationship field. » Impossible to raise an error when an `include` is requested for an inaccessible relationship field.
Status: Postponed » Needs review
StatusFileSize
new1.14 KB

I don't think this is postponed on any particular issue anymore?

Also, making this test/patch pass is AFAICT the goal.

wim leers’s picture

StatusFileSize
new4.18 KB
new5.3 KB

@gabesullice told me in chat he thought that he thought/hoped #2976108: Spec Compliance: Impossible to include related resources when relationship field is not in a request's sparse fieldset could serve as an inspiration for how to implement this. And I apparently said something similar in #11. I'm happy to say that turned out to be true :)

The IS says:

  1. Decide if this should be a 400-level bad request, or if an error should be added to meta.errors.
  2. Implement the change.

I'm assuming it's the latter: an error added to meta.errors.

Assuming that, this patch is a starting point. The bubbling of meta.errors from deeper levels to the top level is a bit messy, because, to my great surprise, that is not something that is actually supported yet! :O

wim leers’s picture

Status: Needs review » Needs work
+++ b/src/Normalizer/HttpExceptionNormalizer.php
@@ -59,7 +59,7 @@ class HttpExceptionNormalizer extends NormalizerBase {
-      FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+      1,

This caused those failures. Fixing…

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new5.11 KB
wim leers’s picture

Assigned: gabesullice » wim leers

Also working to fix the remaining failures.

wim leers’s picture

Assigned: wim leers » Unassigned
StatusFileSize
new6.04 KB
new9.33 KB

This fixes most but not all failures.

gabesullice’s picture

[This] is a bit messy, because, to my great surprise, that is not something that is actually supported yet! :O

That's what I was poorly trying to get across in #9; it's caused so much pain in testing, as evidenced by all the removed unsets and foreach loops in #22's interdiff. I'm SO happy to see those deletions! :)

gabesullice’s picture

  1. +++ b/src/Normalizer/EntityReferenceFieldNormalizer.php
    @@ -86,6 +87,13 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
    +      $field_name = $field->getName();
    ...
    +      if (isset($context['include']) && in_array($field_name, $context['include'], TRUE)) {
    

    I think this may need to pass through ResourceType::getPublicName() unless $context['include'] already went through the reverse process.

    I wish we had a class InternalFieldName extends \SplString and ExternalFieldName class to help track these values and their transformations as they make their way through the codebase. What would you think of that (here, or as a followup)?

  2. +++ b/src/Normalizer/Value/EntityNormalizerValue.php
    @@ -108,6 +109,11 @@ class EntityNormalizerValue implements ValueExtractorInterface, CacheableDepende
    +        $previous_errors = NestedArray::getValue($rasterized, ['meta', 'errors']) ?: [];
    

    I really need to use this pattern more often. I always forget about NestedArray.

  3. +++ b/src/Normalizer/Value/EntityNormalizerValue.php
    @@ -108,6 +109,11 @@ class EntityNormalizerValue implements ValueExtractorInterface, CacheableDepende
    +        $rasterized['meta']['errors'] = array_merge($previous_errors, $normalizer_value->rasterizeValue());
    

    Since $previous_errors is a list of errors, I think we want to push the rasterized value onto that array, not merge it into the list array. PHP--

  4. +++ b/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php
    @@ -123,7 +123,14 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface,
    +          assert(['errors'] === array_keys($rasterized_value['meta']));
    

    I think this is to ensure that nobody overrides the normalization process and adds their own metadata?

  5. +++ b/src/Normalizer/Value/JsonApiDocumentTopLevelNormalizerValue.php
    @@ -123,7 +123,14 @@ class JsonApiDocumentTopLevelNormalizerValue implements ValueExtractorInterface,
    +          $rasterized['meta']['errors'] = array_merge($previous_errors, $rasterized_value['meta']['errors']);
    

    same as above.

  6. +++ b/tests/src/Functional/ResourceResponseTestTrait.php
    @@ -491,7 +491,7 @@ trait ResourceResponseTestTrait {
    -      $error['source']['pointer'] = ($pointer) ? $pointer : $relationship_field_name;
    +      $error['source']['pointer'] = ($pointer) ? $pointer : "/data/relationships/$relationship_field_name";
    

    ❤️

  7. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1126,12 +1126,6 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -    // @todo remove this loop in https://www.drupal.org/project/jsonapi/issues/2853066.
    
    @@ -1157,12 +1151,6 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -    // @todo remove this loop in https://www.drupal.org/project/jsonapi/issues/2853066.
    
    @@ -1193,12 +1181,6 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -    // @todo remove this loop in https://www.drupal.org/project/jsonapi/issues/2853066.
    
    @@ -2466,11 +2448,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -          // @todo remove this when inaccessible relationships are able to raise errors in https://www.drupal.org/project/jsonapi/issues/2956084.
    

    😍

gabesullice’s picture

+++ b/tests/src/Functional/ResourceTestBase.php
@@ -1617,7 +1617,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
-        // @todo remove this conditional when inaccessible relationships are able to raise errors.
+        // @todo remove this when inaccessible relationships are able to raise errors in https://www.drupal.org/project/jsonapi/issues/2956084.
         if (strpos($error['detail'], 'The current user is not allowed to view this relationship.') !== 0) {
           $expected_document['meta']['errors'][] = $error;
         }

The code above is part of the interdiff at #2972808-8: Comprehensive JSON API integration test coverage phase 6: POST/PATCH/DELETE of relationships and its counterpart should be removed here along with the other modified expectations.

wim leers’s picture

StatusFileSize
new1.03 KB
new10.33 KB

This fixes NodeTest::getRelated() and NodeTest::getRelationships(). I'm not convinced this is what we want though, but there already is a pre-existing todo for that very seem reason, about the change this interdiff is making…

Status: Needs review » Needs work

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

wim leers’s picture

Status: Needs work » Needs review
Issue tags: +blocker
StatusFileSize
new2.7 KB
new12.39 KB

I actually wanted to shift my focus from this to #2955020-17: Spec Compliance: JSON API's profile/extention (Fancy Filters, Drupal sorting, Drupal pagination, relationship arity) needs to be explicitly communicated, but given that this blocks #2972808: Comprehensive JSON API integration test coverage phase 6: POST/PATCH/DELETE of relationships, I spent some more time on it. For the first time NodeTest will pass all tests. Hopefully some others will too!

Status: Needs review » Needs work

The last submitted patch, 28: 2956084-28.patch, failed testing. View results

wim leers’s picture

Assigned: Unassigned » wim leers

Back on this.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.51 KB
new13.87 KB

This fixes TermTest::testGetIndividual().

More in the morning.

Status: Needs review » Needs work

The last submitted patch, 31: 2956084-31.patch, failed testing. View results

wim leers’s picture

Quoting #4:

Should this be pushed to 2.x too? Or can/should we do it in 1.x, because this is something that currently just is broken/fails? Can it be done elegantly in 1.x, or would it be much easier to just do it all in 2.x?

Quoting #6:

I can't see a way to do this in 1.x without wasting a ton of effort.

So #6 moved this from 1.x to 2.x.

Yet this apparently is a blocker for #2972808: Comprehensive JSON API integration test coverage phase 6: POST/PATCH/DELETE of relationships, and that issue is against 1.x. So … I'm confused now.

gabesullice’s picture

Version: 8.x-2.x-dev » 8.x-1.x-dev
I can't see a way to do this in 1.x without wasting a ton of effort.

So #6 moved this from 1.x to 2.x.

I couldn't see a way to do this "without wasting a ton of effort"; however:

1) you've found a way to do this with a lot less effort than I imagined (I thought it'd practically be a rewrite of the normalizers)
2) that effort isn't wasted if it unblocks #2946537: Test coverage: Inclusion of intermediate resources when include is a multi-part relationship path (you linked #2972808 in #33 by mistake I think).

IOW, before it was a blocker for test coverage, the decision reached in #2853066: Spec Compliance: Inaccessible collection/related resources surface errors: should be 200 with hypermedia + metadata was to change the way errors are put in the document. That change would be in 2.x. So spending time to make this work properly in a fashion that we already had decided to replace seemed frivolous.

wim leers’s picture

Status: Needs work » Needs review
Related issues: +#2946537: Test coverage: Inclusion of intermediate resources when include is a multi-part relationship path
StatusFileSize
new524 bytes
new14.37 KB

I didn't link to #2972808: Comprehensive JSON API integration test coverage phase 6: POST/PATCH/DELETE of relationships by mistake, I thought this was a blocker to that issue! Based on #2972808-8: Comprehensive JSON API integration test coverage phase 6: POST/PATCH/DELETE of relationships. But as I explained in #2972808-22: Comprehensive JSON API integration test coverage phase 6: POST/PATCH/DELETE of relationships, that was a misinterpretation on my side! Sorry :(

Thanks for explaining that this is a blocker, but not for #2972808, for #2946537: Test coverage: Inclusion of intermediate resources when include is a multi-part relationship path!


I am still digging into the 6 remaining failures. Man, they're hard to fix. I've fixed one more failure, I'll just post that rather than waiting for all of them to be fixed. This fixes MediaTest::testGetIndividual().

Status: Needs review » Needs work

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

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.07 KB
new15.42 KB

Fix FileTest::testGetIndividual() and CommentTest::testGetIndividual() in a similar way.

Status: Needs review » Needs work

The last submitted patch, 37: 2956084-37.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new3.14 KB
new16.88 KB

All this time, I was trying to get the expected pointer to match the actual one. And that got me very deep into the details of the test coverage that automatically constructs expected responses for collections, and includes for collections; those expected responses are constructed by following the web of responses. This is understandably and necessarily complex.

But then it hit me: I can just grant additional permissions. For some reason, getIncludePermissions() was only being granted for includes for the individual test coverage, not for includes for the collection test coverage. This seems like a simple oversight.

This should make all tests pass except UserTest::testCollection and EntityTest::testCollection().

Status: Needs review » Needs work

The last submitted patch, 39: 2956084-39.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.21 KB
new18.05 KB

Yay, down to 2 failures as expected! 🎉

EntityTest::testCollection() needed special care. I expected the fix to be similar to those in #39. But that didn't work out. I found the reason: its owner is user zero, the anonymous user. view access to the anonymous user is always forbidden by UserAccessControlHandler. If #2843922: Show label of inaccessible entities ('view' access denied) when 'view label' access is allowed were already fixed, this could've worked.

But the solution is simple: just don't make user zero the owner! That was a non-essential detail of the test coverage anyway.

Status: Needs review » Needs work

The last submitted patch, 41: 2956084-41.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new2.05 KB
new19.58 KB

The last one: UserTest::testCollection(). Not only are the pointers wrong, there's also a difference in error count! 7 versus 9.

Applying the same pattern as #39 and #41 fixes the pointers problem and in doing so, it reduces the number of errors, but the delta remains: 4 versus 6.

The count delta is due to the sorted collection containing user zero, and the way the expected collection response is built; it is fetching related data (to fetch the expected includes) for every entity in the collection, even if the entity is not accessible. But if the entity is not accessible, then neither can its includes be accessible; otherwise that'd be information disclosure. In other words: things are working correctly and securely, but the expectations generated by the tests are wrong :) A simple if-test in the right place fixes this!

This should be green 🙏

gabesullice’s picture

+++ b/tests/src/Functional/ResourceTestBase.php
@@ -1148,7 +1148,10 @@ abstract class ResourceTestBase extends BrowserTestBase {
-    $related_responses = array_reduce($sorted_entity_collection, function ($related_responses, $entity) use ($relationship_field_names, $request_options) {
+    $related_responses = array_reduce($sorted_entity_collection, function ($related_responses, EntityInterface $entity) use ($relationship_field_names, $request_options) {
+      if (!$entity->access('view', $this->account)) {
+        return $related_responses;
+      }

This makes complete sense. Good explanation.

Status: Needs review » Needs work

The last submitted patch, 43: 2956084-43.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.5 KB
new19.47 KB

In #41, UserTest::testCollection() failed. In #43, this is fixed, but now UserTest::testGetIndividual() is failing. Why?

+++ b/tests/src/Functional/UserTest.php
@@ -461,4 +461,13 @@ class UserTest extends ResourceTestBase {
+      'roles' => ['administer users'],

This causes many additional fields to show up in testGetIndividual(), which then causes the doTestIncluded()
getExpectedIncludeResponse()getExpectedGetIndividualResourceResponse()getExpectedDocument() call chain to get a set of fields that is only a subset of what is now returned.

Gah! 😭


Turns out we didn't even that administer users permission; the errors match exactly, they're just sorted differently! That's hopefully fixable by updating \Drupal\Tests\jsonapi\Functional\ResourceTestBase::sortErrors() :)

gabesullice’s picture

Status: Needs review » Needs work

GREAT work! I'm sure that was tedious and hair-pulling to accomplish.

Can you address #24.3?

  1. +++ b/src/Normalizer/EntityReferenceFieldNormalizer.php
    @@ -86,6 +87,13 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
    +      // Allow includes for inaccessible fields to raise an error.
    

    "Allow inaccessible includes to raise an error"

    Would that be correct? If so, it's easier to understand.

  2. +++ b/src/Normalizer/RelationshipItemNormalizer.php
    @@ -82,6 +83,9 @@ class RelationshipItemNormalizer extends FieldItemNormalizer {
    +      if ($entity_and_access['entity'] instanceof EntityAccessDeniedHttpException) {
    +        $entity_and_access['entity'] = new EntityAccessDeniedHttpException($target_entity, $entity_and_access['access'], "/data/relationships/$host_field_name", $entity_and_access['entity']->getMessage());
    +      }
    

    A comment here would go a long way. As I understand it, you're doing this to change the pointer. Perhaps there should just be a method called setPointer on the exception class.

  3. +++ b/src/Normalizer/Value/EntityNormalizerValue.php
    @@ -108,6 +109,11 @@ class EntityNormalizerValue implements ValueExtractorInterface, CacheableDepende
    +        $rasterized['meta']['errors'] = array_merge($previous_errors, $normalizer_value->rasterizeValue());
    

    This is mentioned in #24.3

  4. +++ b/tests/src/Functional/EntityTestTest.php
    @@ -186,4 +186,13 @@ class EntityTestTest extends ResourceTestBase {
    +  protected static function getIncludePermissions() {
    +    return [
    +      'user_id' => ['administer users'],
    +    ];
    +  }
    

    Can't this just be access user profiles?

  5. +++ b/tests/src/Functional/MediaTest.php
    @@ -369,4 +369,16 @@ class MediaTest extends ResourceTestBase {
    +    return [
    +      'bundle' => ['administer media types'],
    +      'field_media_file' => ['access content'],
    +      'thumbnail' => ['access content'],
    +      'uid.roles' => ['administer users'],
    

    My main concern with granting all these permissions to solve the test failures is that we're simply silencing the associated access "error" rather than asserting that it bubbled up correctly.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new6.42 KB
new24.59 KB

While working on this, I spotted a fair amount of duplication that we've accumulated, and which seriously complicated the process of getting to a green patch. This works at least for UserTest::testGetIndividual(), let's see if it works for everything!

I think there's more to be done still, but the functional overlap between getExpectedIncludeResponse() and decorateExpectedResponseForIncludedFields() is significant.

I suspect getExpectedRelatedResponses() and getExpectedIncludedResourceResponse() are similarly overlapping.

Note that this probably should happen in a separate issue, but having done so much digging in this issue, I didn't want this insight to get lost, and posting a separate patch will mean less (since this issue is improving a fair bit of our test coverage). If this is green, it should probably be omitted from this patch and spun off into its own issue. Unless @gabesullice thinks it belongs here.

wim leers’s picture

+++ b/src/Normalizer/RelationshipItemNormalizer.php
@@ -82,6 +83,9 @@ class RelationshipItemNormalizer extends FieldItemNormalizer {
+        $entity_and_access['entity'] = new EntityAccessDeniedHttpException($target_entity, $entity_and_access['access'], "/data/relationships/$host_field_name", $entity_and_access['entity']->getMessage());

I'm actually not at all convinced that /data/relationships/… is the right pointer here.

If it's for an include, the pointer should probably be /includes?

That being said… I don't think it makes sense to fix that here. That's going to be another massive undertaking.

wim leers’s picture

Assigned: wim leers » Unassigned
StatusFileSize
new5.04 KB
new26.89 KB

Addressing all the reviews now!

#24:

  1. 👍 You're right! Re-reading \Drupal\jsonapi\Normalizer\JsonApiDocumentTopLevelNormalizer::expandContext() seems to confirm your suspicion. Because the variable is called $public_includes. But re-reading \Drupal\jsonapi\Context\FieldResolver::resolveInternalIncludePath() indicates the opposite! Therefore $context['includes'] lists internal, resolved includes. This is also the only sensible thing here; otherwise we'd continuously be translating back and forth everywhere. It makes sense that expandContext() did this for us.

    Renamed the variable in expandContext() to avoid future confusion.

  2. 🙂
  3. 👎 That'd cause nested arrays rather than a single array of errors I think? Both sides are lists of errors. Pushing won't work; that'd I copy/pasted this from \Drupal\jsonapi\Normalizer\Value\JsonApiDocumentTopLevelNormalizerValue::rasterizeValue(). Seems to work fine? Perhaps I'm misunderstanding what you wrote?
  4. 🦅👀 It's to ensure that we never end up omitting data under meta that is not meta.errors. For example meta.openapi. If/when we start doing that, this assertion will fail, which will remind us to update this logic, and we'll be thankful to not have to figure this out all from scratch! It's a safeguard for our future selves :)
  5. See above.
  6. ❤️
  7. ❤️

#47:

  1. ✔️
  2. 👍 BTW, you were right about the reason.
  3. See above.
  4. 👍 Indeed!
  5. Agreed. Which is why I'm not convinced this patch is ready to go as-is. The better solution would be to first assert expected error responses, and then grant the permission for a particular include, then observing that error disappearing. That's how we test field access.

    Perhaps you could take that on? :)

Status: Needs review » Needs work

The last submitted patch, 50: 2956084-50.patch, failed testing. View results

wim leers’s picture

composer/packagist are having a bad day:

Updating dependencies (including require-dev)

                                                                                                                                                                                     
  [Composer\Downloader\TransportException]                                                                                                                                           
  The "http://packagist.org/p/symfony/polyfill-iconv%2445594a6036fb36f5574e98f855e18f60cf7bf3e1007cac2b001a9111d12b4c90.json" file could not be downloaded (HTTP/1.1 404 Not Found)  

Retesting.

gabesullice’s picture

While working on this, I spotted a fair amount of duplication that we've accumulated, and which seriously complicated the process of getting to a green patch.
...
If this is green, it should probably be omitted from this patch and spun off into its own issue. Unless @gabesullice thinks it belongs here.

This is definitely tech debt we should pay down, but not here.

#50.24.3: I'm just very surprised that HttpExceptionNormalizerValue returns a list in every case. If you're confident that this is the case, then I am too :)

  1. +++ b/src/Normalizer/RelationshipItemNormalizer.php
    @@ -84,7 +84,7 @@ class RelationshipItemNormalizer extends FieldItemNormalizer {
    +        $entity_and_access['entity']->setPointer("/data/relationships/$host_field_name");
    

    I agree that /includes/$host_field_name would be ideal, but it's not worth addressing, as you said. The pointer concept in the spec is really meant for POST/PATCH request documents that fail validation constraints AFAICT.

  2. +++ b/tests/src/Functional/EntityTestTest.php
    @@ -191,7 +191,8 @@ class EntityTestTest extends ResourceTestBase {
    -      'user_id' => ['administer users'],
    +      'user_id' => ['access user profiles'],
    +      'user_id.roles' => ['administer users'],
    

    👌

Which is why I'm not convinced this patch is ready to go as-is. The better solution would be to first assert expected error responses, and then grant the permission for a particular include, then observing that error disappearing.... Perhaps you could take that on? :)

Hoo boy, sure :S, I'll give it a go this afternoon :P

gabesullice’s picture

gabesullice’s picture

StatusFileSize
new2.24 KB
new27.74 KB

Fixing an outdated method name from #2972808.

gabesullice’s picture

StatusFileSize
new7.71 KB
new32.05 KB

This consolidates a bit of code duplication by putting includes decoration in the expected collection method.

Then, we introduce the code that will test an include collection without authentication. If it should be differ by user/permissions, we then test it again with appropriate permissions added.

gabesullice’s picture

Assigned: gabesullice » Unassigned
Status: Needs work » Needs review
StatusFileSize
new2.67 KB
new32.88 KB

This should address some of the new errors that showed up after re-rolling.


I'm done for the weekend, so unassigning if someone wants to pick this back up. What's likely needed is for the same pattern to be applied to individual includes + any other errors that pop up.

Status: Needs review » Needs work

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

gabesullice’s picture

Assigned: Unassigned » gabesullice
Status: Needs work » Needs review
StatusFileSize
new1.33 KB
new34 KB

Starting up again

Status: Needs review » Needs work

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

gabesullice’s picture

Status: Needs work » Needs review

Alright, looks like the we need to refine the caching expectations.

BlockContent* is probably still failing.

gabesullice’s picture

StatusFileSize
new1.33 KB
new34 KB
gabesullice’s picture

StatusFileSize
new1.95 KB
new34.21 KB

Whoops, you can ignore #62.

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

Status: Needs review » Needs work

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

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new6.03 KB
new35.45 KB

/me crosses fingers!

wim leers’s picture

🤞 → 🤘

gabesullice’s picture

Assigned: gabesullice » Unassigned

Sweet! A review would be great @wim leers or @e0ipso :)

wim leers’s picture

Status: Needs review » Needs work

#55: 👍

#56 + #57:

  1. +++ b/tests/src/Functional/BlockContentTest.php
    @@ -262,4 +262,12 @@ class BlockContentTest extends ResourceTestBase {
    +  protected static function getIncludePermissions() {
    +    return [
    +      'field_jsonapi_test_entity_ref' => ['field_jsonapi_test_entity_ref view access']
    +    ];
    +  }
    

    Why only do this for this entity type? Why not do this in ResourceTestBase?

  2. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1124,17 +1124,10 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -    $expected_response = static::decorateExpectedResponseForIncludedFields($expected_response, $related_responses);
    +    $expected_response =  $this->getExpectedCollectionResponse($filtered_entity_collection, $filtered_collection_include_url->toString(), $request_options, $relationship_field_names);
    
    @@ -1155,14 +1163,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -    $expected_response = static::decorateExpectedResponseForIncludedFields($expected_response, $related_responses);
    +    $expected_response = $this->getExpectedCollectionResponse($sorted_entity_collection, $sorted_collection_include_url->toString(), $request_options, $relationship_field_names);
    
    @@ -1180,13 +1181,16 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -  protected function getExpectedCollectionResponse(array $collection, $self_link, array $request_options) {
    +  protected function getExpectedCollectionResponse(array $collection, $self_link, array $request_options, array $included_paths = NULL) {
    

    Refactoring/cleanup: 👍

  3. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1142,6 +1135,21 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +    // If the response should vary by a user's authorizations, grant permissions
    +    // for the included resources and execute another request.
    +    if (!empty($relationship_field_names) && !empty(array_intersect($expected_cacheability->getCacheContexts(), ['user', 'user.permissions', 'user.roles']))) {
    

    So this is new test coverage:

    Then, we introduce the code that will test an include collection without authentication. If it should be differ by user/permissions, we then test it again with appropriate permissions added.

    Great!

  4. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1142,6 +1135,21 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      $expected_response =  $this->getExpectedCollectionResponse($filtered_entity_collection, $filtered_collection_include_url->toString(), $request_options, $relationship_field_names);
    

    But where is this causing the request to be unauthenticated, like the comment said?

##5:

+++ b/tests/src/Functional/ResourceTestBase.php
@@ -1560,11 +1560,13 @@ abstract class ResourceTestBase extends BrowserTestBase {
+      // @todo: is this the right pointer?
+      $pointer = "/data/relationships/$relationship_field_name";

I made the same remark in #49. I think we can leave that for a future issue? This is still a big step forward.


#66:

+++ b/tests/src/Functional/ResourceTestBase.php
@@ -2867,4 +2859,18 @@ abstract class ResourceTestBase extends BrowserTestBase {
+  protected function grantIncludedPermissions(array $include_paths = []) {

Nit: "included" vs "include"


Overall patch review:

  1. +++ b/src/Controller/EntityResource.php
    @@ -819,7 +819,7 @@ class EntityResource {
           // @todo Is this really the right path?
    
    +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1549,11 +1562,13 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      // @todo: is this the right pointer?
    

    We should replace these @todos with an issue link.

  2. +++ b/src/Normalizer/EntityReferenceFieldNormalizer.php
    @@ -86,6 +87,13 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
    +      $field_name = $field->getName();
    +      // Allow inaccessible includes to raise an error.
    +      if (isset($context['include']) && in_array($field_name, $context['include'], TRUE)) {
    +        $exception = new EntityAccessDeniedHttpException($field->getEntity(), $field_access, "/data/relationships/$field_name", 'The current user is not allowed to view this relationship.');
    +        return $this->serializer->normalize($exception, $format, $context);
    +      }
    

    This is the key/central change that triggered all other changes.

  3. +++ b/src/Normalizer/RelationshipItemNormalizer.php
    @@ -82,6 +83,9 @@ class RelationshipItemNormalizer extends FieldItemNormalizer {
    +      if ($entity_and_access['entity'] instanceof EntityAccessDeniedHttpException) {
    +        $entity_and_access['entity']->setPointer("/data/relationships/$host_field_name");
    +      }
    

    Could also use a link to that same issue.

  4. +++ b/src/Normalizer/RelationshipItemNormalizer.php
    --- a/src/Normalizer/Value/EntityNormalizerValue.php
    +++ b/src/Normalizer/Value/EntityNormalizerValue.php
    

    In here, bugfixes to how includes are rasterized; necessary to let includes bubble up meta errors!

  5. +++ b/tests/src/Functional/FileTest.php
    @@ -230,4 +230,14 @@ class FileTest extends ResourceTestBase {
    +  protected static function getIncludePermissions() {
    +    return [
    +      'uid' => ['access user profiles'],
    +      'uid.roles' => ['administer users'],
    +    ];
    +  }
    
    +++ b/tests/src/Functional/MediaTest.php
    @@ -374,9 +374,21 @@ class MediaTest extends ResourceTestBase {
    +  protected static function getIncludePermissions() {
    +    return [
    +      'bundle' => ['administer media types'],
    +      'field_media_file' => ['access content'],
    +      'thumbnail' => ['access content'],
    +      'uid.roles' => ['administer users'],
    +    ];
       }
    
    +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1127,24 +1127,31 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +    // If the response should vary by a user's authorizations, grant permissions
    +    // for the included resources and execute another request.
    +    $varies_by_access = !empty(array_intersect($expected_cacheability->getCacheContexts(), ['user', 'user.permissions', 'user.roles']));
    +    if (!empty($relationship_field_names) && $varies_by_access) {
    +      $applicable_permissions = array_intersect_key(static::getIncludePermissions(), array_flip($relationship_field_names));
    +      $flattened_permissions = array_unique(array_reduce($applicable_permissions, 'array_merge', []));
    +      $this->grantPermissionsToTestedRole($flattened_permissions);
    +      $expected_response =  $this->getExpectedCollectionResponse($filtered_entity_collection, $filtered_collection_include_url->toString(), $request_options, $relationship_field_names);
    +      $expected_cacheability = $expected_response->getCacheableMetadata();
    +      $expected_document = $expected_response->getResponseData();
    +      $response = $this->request('GET', $filtered_collection_include_url, $request_options);
    +      $requires_include_only_permissions = !empty($flattened_permissions);
    +      $cacheable = $expected_cacheability->getCacheMaxAge() !== 0;
    +      $dynamic_cache = $cacheable ? $requires_include_only_permissions ? 'MISS' : 'HIT' : 'UNCACHEABLE';
    +      $this->assertResourceResponse(200, $expected_document, $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), FALSE, $dynamic_cache);
    +    }
    

    I wrote:

    Which is why I'm not convinced this patch is ready to go as-is. The better solution would be to first assert expected error responses, and then grant the permission for a particular include, then observing that error disappearing.... Perhaps you could take that on? :)

    And I think you added the cited new test code to test that. But does it actually test this? I don't think it does.

  6. +++ b/tests/src/Functional/ResourceResponseTestTrait.php
    @@ -491,7 +514,7 @@ trait ResourceResponseTestTrait {
    -      $error['source']['pointer'] = ($pointer) ? $pointer : $relationship_field_name;
    +      $error['source']['pointer'] = ($pointer) ? $pointer : "/data/relationships/$relationship_field_name";
    

    Again point to that same issue.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new36.44 KB
wim leers’s picture

Status: Needs review » Needs work

#69 still needs to be addressed.

wim leers’s picture

Version: 8.x-1.x-dev » 8.x-2.x-dev
wim leers’s picture

gabesullice’s picture

Status: Needs work » Needs review
Issue tags: -blocker
StatusFileSize
new2.71 KB

Since #70, `meta.errors` has been removed in favor of "omissions". Also, the include process was rewritten in #2997600: Resolve included resources prior to normalization.

Both of those issue should have made this significantly easier, but it also means that #70 is no longer applicable and will be practically impossible to reroll.

This patch just enables commented out expectations.

Status: Needs review » Needs work

The last submitted patch, 74: 2956084-74.patch, failed testing. View results

wim leers’s picture

Both of those issue should have made this significantly easier

👍

but it also means that #70 is no longer applicable and will be practically impossible to reroll.

Makes sense.

gabesullice’s picture

StatusFileSize
new1.29 KB
new4 KB

This gets things working more, but not perfectly.

The via link needs to be more specific.

gabesullice’s picture

Status: Needs work » Needs review

Queuing tests.

Status: Needs review » Needs work

The last submitted patch, 77: 2956084-77.patch, failed testing. View results

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new972 bytes
new4.27 KB

#77 actually doespass tests, but fails on a single CS violation!

Fixing that.

wim leers’s picture

+++ b/src/IncludeResolver.php
@@ -103,7 +104,11 @@ class IncludeResolver {
-        if (!$field_list->access('view')) {
+        $field_access = $field_list->access('view', NULL, TRUE);
+        if (!$field_access->isAllowed()) {
+          $message = 'The current user is not allowed to view this relationship.';
+          $exception = new EntityAccessDeniedHttpException($entity, $field_access, '', $message);
+          $includes = EntityCollection::merge($includes, new EntityCollection([$exception]));
           continue;

What can I say? This is now all that's necessary.

Compare that to #70. It's night and day!

But in #70, we're also adding getIncludePermissions() and adding test coverage:

+++ b/tests/src/Functional/ResourceTestBase.php
@@ -1127,24 +1127,31 @@ abstract class ResourceTestBase extends BrowserTestBase {
+    // If the response should vary by a user's authorizations, grant permissions
+    // for the included resources and execute another request.
+    $varies_by_access = !empty(array_intersect($expected_cacheability->getCacheContexts(), ['user', 'user.permissions', 'user.roles']));
+    if (!empty($relationship_field_names) && $varies_by_access) {
…

Shouldn't we retain those things?

Status: Needs review » Needs work

The last submitted patch, 80: 2956084-80.patch, failed testing. View results

wim leers’s picture

Oops, I misinterpreted #77 — I thought it was passing tests, apparently it wasn't!

gabesullice’s picture

Assigned: Unassigned » gabesullice

I should have assigned this to myself, it obviously still a WIP

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new5.19 KB
new8.53 KB

This should be closer, if not complete.

Status: Needs review » Needs work

The last submitted patch, 85: 2956084-85.patch, failed testing. View results

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new2.59 KB
new10.69 KB

This asserts that includes on LabelOnlyEntities can also raise an omission. (Yes, in case you were curious, it was super subtle and frustrating to debug 🙃)

Status: Needs review » Needs work

The last submitted patch, 87: 2956084-87.patch, failed testing. View results

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new10.72 KB
new940 bytes

Another little fix.

Status: Needs review » Needs work

The last submitted patch, 89: 2956084-89.patch, failed testing. View results

wim leers’s picture

super subtle and frustrating to debug 🙃

I believe you!

Down to 2 failures :) Patch looks good! Just one nit for now:

+++ b/src/Exception/EntityAccessDeniedHttpException.php
@@ -46,18 +46,22 @@ class EntityAccessDeniedHttpException extends CacheableAccessDeniedHttpException
+   *   (Optional) A relationship field name if access was denied for a

s/for a/for an entity included via a/

gabesullice’s picture

This should make the last tests pass.

The suggestion in #91 does not make sense. You actually might have access to the entity included via a relationship. The specific access issue is that you're not allowed to view the field which targets that entity. I tried to clarify the comment. (n.b., this kind of confusion is why I created #2997594: Introduce a more generic exception class).

Next comment should address #81 (or after some investigation, say why it should be addressed here).

gabesullice’s picture

StatusFileSize
new1.76 KB
new11.48 KB

D'oh!

gabesullice’s picture

StatusFileSize
new13.78 KB
new24.46 KB

This restores the assertions introduced in #56 through #66, as @Wim Leers suggested in #81.

gabesullice’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

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

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new25.68 KB
new3.22 KB
new1.75 KB

This fixes the changes in #94 to account for changes in the codebase since #66.

It also fixes the CS violations in #94.

gabesullice’s picture

Assigned: gabesullice » Unassigned

I.. I think this issue might finally be coming to a close! 😲

wim leers’s picture

StatusFileSize
new610 bytes
  1. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1131,11 +1132,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -    $expected_response = $this->getExpectedCollectionResponse($filtered_entity_collection, $filtered_collection_include_url->toString(), $request_options);
    -    $related_responses = array_reduce($filtered_entity_collection, function ($related_responses, $entity) use ($relationship_field_names, $request_options) {
    -      return array_merge($related_responses, array_values($this->getExpectedRelatedResponses($relationship_field_names, $request_options, $entity)));
    -    }, []);
    -    $expected_response = static::decorateExpectedResponseForIncludedFields($expected_response, $related_responses);
    +    $expected_response = $this->getExpectedCollectionResponse($filtered_entity_collection, $filtered_collection_include_url->toString(), $request_options, $relationship_field_names);
    
    @@ -1157,11 +1175,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -    $expected_response = $this->getExpectedCollectionResponse($sorted_entity_collection, $sorted_collection_include_url->toString(), $request_options);
    -    $related_responses = array_reduce($sorted_entity_collection, function ($related_responses, $entity) use ($relationship_field_names, $request_options) {
    -      return array_merge($related_responses, array_values($this->getExpectedRelatedResponses($relationship_field_names, $request_options, $entity)));
    -    }, []);
    -    $expected_response = static::decorateExpectedResponseForIncludedFields($expected_response, $related_responses);
    +    $expected_response = $this->getExpectedCollectionResponse($sorted_entity_collection, $sorted_collection_include_url->toString(), $request_options, $relationship_field_names);
    
    @@ -1180,13 +1194,16 @@ abstract class ResourceTestBase extends BrowserTestBase {
    -  protected function getExpectedCollectionResponse(array $collection, $self_link, array $request_options) {
    +  protected function getExpectedCollectionResponse(array $collection, $self_link, array $request_options, array $included_paths = NULL) {
    

    Makes sense to move this logic into the helper, because the logic became more nuanced.

  2. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -1144,6 +1141,27 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +      $dynamic_cache = $cacheable ? $requires_include_only_permissions ? 'MISS' : 'HIT' : 'UNCACHEABLE';
    

    Wow 😵

  3. +++ b/tests/src/Functional/ResourceTestBase.php
    @@ -2825,4 +2806,18 @@ abstract class ResourceTestBase extends BrowserTestBase {
    +   * Grant authorization to view includes.
    

    Übernit: s/Grant/Grants/. Will fix on commit. Interdiff already attached.

  4. +++ b/tests/src/Functional/TermTest.php
    @@ -496,4 +496,13 @@ class TermTest extends ResourceTestBase {
    +  protected static function getIncludePermissions() {
    +    return [
    +      'vid' => ['access taxonomy overview'],
    +    ];
    

    Interesting that we only needed this for taxonomy_term. In #66, we needed this for many more fields. Why is that?

Point 3 is the only reason that this isn't RTBC and committed yet.

gabesullice’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new438 bytes
new26.87 KB

I spent some time looking into #99.3. I simply copied that from the interdiffs between #56 and #66. At one point it appeared to fix something. But I've removed it and tests pass locally. I don't know why it was needed, but it isn't any more. Removed.

I applied the #99 interdiff to this patch. Thanks!

Per #9, setting to RTBC.

wim leers’s picture

Assigned: Unassigned » wim leers

I still want to verify this in detail before committing. Just to be 100% sure, and confirm my own understanding.

I expect to commit this tomorrow :)

  • Wim Leers committed ced6284 on 8.x-2.x authored by gabesullice
    Issue #2956084 by gabesullice, Wim Leers: Impossible to raise an error...
wim leers’s picture

Status: Reviewed & tested by the community » Fixed
Related issues: +#3005158: Nested include test coverage is inadequate wrt "include permissions"

Reverted the README changes that accidentally sneaked in to #100.

+++ b/tests/src/Functional/NodeTest.php
@@ -346,7 +346,7 @@ class NodeTest extends ResourceTestBase {
-      'uid.roles' => ['administer permissions'],
+      'uid.roles' => ['administer users'],

This is a fix that as in the earlier patch iterations, introduced in #28. If you check \Drupal\user\UserAccessControlHandler::checkFieldAccess(), you can see that administer users is definitely the right one.

So some change in the earlier patch iterations was causing getIncludePermissions() to be used correctly, in HEAD its' being used incorrectly. I managed to reproduce that same flaw with the current patch iteration. But also with HEAD. We shouldn't block this issue on fixing a pre-existing bug. Created a new issue for that: #3005158: Nested include test coverage is inadequate wrt "include permissions".

wim leers’s picture

Assigned: wim leers » Unassigned
wim leers’s picture

gabesullice’s picture

🎉🎉🎉🎉🎉🎉

> 100 comments, multiple patch approaches, numerous blockers and more than 6 months since the initial report. Finally!

wim leers’s picture

:)

Status: Fixed » Closed (fixed)

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