Problem/Motivation

Discovered while working on #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method. See #2737719-91: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method.

Updating an entity via REST fails when the entity has a certain field value that the current user may not set, even if the PATCH request does NOT modify that field.

For example: node body uses a certain format that user A is not allowed to use, but when user A PATCHes only the node title, they still get a validation error.

Proposed resolution

If a REST request sends a field that the user has access to but is unchanged from the current field value the validation should not be run on this field.

Of course, we need to be careful to not disclose information too: we don't want to make it possible to guess the current invalid value of a field if the user is sending a single field in a PATCH request: if we respond with a 200 then because there's nothing to change, we've disclosed the value of a field that is inaccessible to the user (pointed out by @tstoeckler in #95!).

A generic example through which we can explain the desired behavior:: an entity with a reference field that contains a reference to a since then deleted entity.

  • If user A sends the current value of that field, but is not allowed to view the reference field, then they should still get a validation error (because otherwise information disclosure).
  • But if user B sends that same request body, and is allowed to view the reference field, then they should not get a validation error (because they're allowed to see the value, and are simply sending the current value, which means nothing is being changed, even if the current value is invalid).

All of this of course needs full test coverage, and the patch does :)

Remaining tasks

None.

User interface changes

None.

API changes

None.

Data model changes

None.

CommentFileSizeAuthor
#109 2821077-109.patch12.06 KBtedbow
#109 interdiff-105-109.txt3.56 KBtedbow
#105 2821077-105.patch13.87 KBwim leers
#105 interdiff.txt874 byteswim leers
#101 2821077-100.patch13.85 KBwim leers
#101 interdiff.txt3.32 KBwim leers
#98 2821077-98.patch13.65 KBwim leers
#98 interdiff.txt2.29 KBwim leers
#97 2821077-97.patch12.47 KBwim leers
#97 interdiff.txt1.2 KBwim leers
#93 2821077-93.patch12.02 KBwim leers
#93 interdiff.txt830 byteswim leers
#92 2821077-92.patch11.84 KBwim leers
#92 interdiff.txt1.13 KBwim leers
#89 2821077-89.patch10.77 KBwim leers
#89 interdiff.txt851 byteswim leers
#88 2821077-88.patch11.55 KBwim leers
#88 interdiff.txt1.13 KBwim leers
#86 2821077-86.patch11.46 KBwim leers
#81 2821077-81.patch11.44 KBwim leers
#81 interdiff-80-81.txt1.02 KBwim leers
#80 2821077-80.patch11.47 KBwim leers
#80 interdiff-79-80.txt1.58 KBwim leers
#79 interdiff.txt3.23 KBwim leers
#79 2821077-79.patch11.39 KBwim leers
#77 2821077-77.patch12.24 KBwim leers
#77 interdiff.txt4.53 KBwim leers
#76 2821077-76.patch13 KBwim leers
#76 interdiff.txt2.98 KBwim leers
#75 2821077-75.patch14.72 KBwim leers
#70 interdiff-70.txt4.34 KBamateescu
#62 2821077-62.patch14.72 KBwim leers
#62 interdiff.txt3.6 KBwim leers
#61 2821077-61.patch14.8 KBwim leers
#61 interdiff.txt3.82 KBwim leers
#60 2821077-60.patch13.89 KBwim leers
#60 interdiff.txt1.11 KBwim leers
#58 2821077-58.patch14.39 KBwim leers
#58 interdiff.txt1.14 KBwim leers
#56 2821077-55.patch14.33 KBwim leers
#56 interdiff-48-55.txt1.26 KBwim leers
#56 interdiff.txt2.88 KBwim leers
#49 2821077-49.patch14.77 KBwim leers
#49 interdiff.txt3.67 KBwim leers
#48 interdiff-2821077.txt3.46 KBdawehner
#48 2821077-48.patch13.87 KBdawehner
#40 interdiff-2821077.txt12.26 KBdawehner
#40 2821077-40.patch13.54 KBdawehner
#36 interdiff-36.txt6.02 KBdawehner
#36 2821077-36.patch19.89 KBdawehner
#34 interdiff-2821077.txt1.53 KBdawehner
#34 2821077-34.patch13.88 KBdawehner
#32 2821077-32.patch12.72 KBdawehner
#32 interdiff-2821077.txt2.5 KBdawehner
#25 2821077-25.patch10.98 KBwim leers
#25 interdiff.txt6.07 KBwim leers
#24 interdiff-2821077.txt1.98 KBdawehner
#24 2821077-24.patch11.1 KBdawehner
#22 2821077-22.patch11.17 KBdawehner
#18 interdiff-2821077.txt1.86 KBdawehner
#18 2821077-18.patch11.17 KBdawehner
#16 interdiff-2821077.txt1.54 KBdawehner
#16 2821077-16.patch9.61 KBdawehner
#15 interdiff-2821077.txt8.22 KBdawehner
#15 2821077-15.patch10.77 KBdawehner
#10 interdiff-2821077.txt4.38 KBdawehner
#10 2821077-10.patch3.79 KBdawehner
#4 2821077-4.patch2.2 KBdawehner

Comments

Wim Leers created an issue. See original summary.

wim leers’s picture

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

dawehner’s picture

Status: Active » Needs review
StatusFileSize
new2.2 KB

Here is some quick start for that.

wim leers’s picture

Issue tags: +Needs tests

Ohhh! That looks easier than I expected! Thanks, great start :)

Also, it looks like this might not be blocked on #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior, which is what I thought at first. I was actually planning to mark this postponed because of that. Happy to be wrong!

dawehner’s picture

This really just reuses the methods which have been also used for the HTML form, see \Drupal\Core\Entity\Entity\EntityFormDisplay::validateFormValues.

In general though this should have some form of security review. Its something which should be better safe than sorry.

wim leers’s picture

Issue tags: +Needs security review
wim leers’s picture

Assigned: Unassigned » tstoeckler

This needs Entity/Field API maintainer feedback. Summoning @tstoeckler :)

tstoeckler’s picture

Assigned: tstoeckler » Unassigned
Status: Needs review » Needs work

Yes, conceptually this makes sense. Like #6 says, this is equivalent to the form handling. See also \Drupal\Core\Entity\ContentEntityForm::validateForm() for another usage. Thus, unassigning.

Some notes on the actual patch:

  1. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -297,6 +297,7 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    +    $changed_fields = array_keys($entity->_restSubmittedFields);
         $this->validate($original_entity);
    

    Let's actually use $changed_fields ;-)

  2. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
    @@ -19,10 +19,14 @@
    +   * @param string[] $changed_fields
    +   *   An array of changed fields. If specified filter out the non applying
    +   *   violations.
    ...
    -  protected function validate(EntityInterface $entity) {
    +  protected function validate(EntityInterface $entity, array $changed_fields = []) {
    

    So passing an empty list list of fields actually does no filtering at all. That kind of makes sense, but is also somewhat confusing so I think we should at least document this. Alternatively we could do some NULL vs. [] dance, but not sure that is better...

  3. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
    @@ -33,6 +37,13 @@ protected function validate(EntityInterface $entity) {
    +      $entity_level_violations = $violations->getEntityViolations();
    +      $entity_level_violations->addAll($violations->filterByFields($changed_fields));
    +      $violations = $entity_level_violations;
    

    Why do we need to do this "workaround" of fetching the entity-level violations? Why is $violations->filterByFields($changed_fields) not sufficient?

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new3.79 KB
new4.38 KB

So passing an empty list list of fields actually does no filtering at all. That kind of makes sense, but is also somewhat confusing so I think we should at least document this. Alternatively we could do some NULL vs. [] dance, but not sure that is better...

Good point. Let's refactor the code to no longer require this weird dance ...

Let's actually use $changed_fields ;-)

Heh fair point.

Why do we need to do this "workaround" of fetching the entity-level violations? Why is $violations->filterByFields($changed_fields) not sufficient?

As far as I understand its used in the form to be able to separate the violations which have to apply to certain widgets from other errors.

wim leers’s picture

Awesome, so glad to see this moving forward!

This will become even better once #2456257: [PP-1] Normalizers must set $entity->_restSubmittedFields for entity POSTing/PATCHing to work lands, which in turn depends on #2862574: Add ability to track an entity object's dirty fields (and see if it has changed). And that last issue makes me wonder whether we should actually be updating \Drupal\Core\Entity\ContentEntityBase::validate() instead? Shouldn't that code be responsible for only validating fields that were actually modified?

dawehner’s picture

This will become even better once #2456257: [PP-1] EntityResource relies on a normalizer's deserialize method adding _restSubmittedFields lands, which in turn depends on #2862574: Add ability to track an entity object's dirty fields. And that last issue makes me wonder whether we should actually be updating \Drupal\Core\Entity\ContentEntityBase::validate() instead? Shouldn't that code be responsible for only validating fields that were actually modified?

We could do that in a follow up IMHO, given that the BC evaluation would be a different one.

wim leers’s picture

Issue tags: +Needs followup

We could do that in a follow up IMHO, given that the BC evaluation would be a different one.

+1

wim leers’s picture

Issue tags: -Needs security review

I think #9 addressed needs security review.

dawehner’s picture

Issue tags: -Needs tests
StatusFileSize
new10.77 KB
new8.22 KB

Here is some test coverage.

Turns out the implementation was wrong. It filters out entries rather does what array_filter does, aka. keep elements unless they meet a certain condition.

dawehner’s picture

StatusFileSize
new9.61 KB
new1.54 KB

I accidentally had some test cases commented out

Status: Needs review » Needs work

The last submitted patch, 16: 2821077-16.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new11.17 KB
new1.86 KB

Let's fix some failing tests ...

Status: Needs review » Needs work

The last submitted patch, 18: 2821077-18.patch, failed testing.

dawehner’s picture

Version: 8.3.x-dev » 8.4.x-dev
Status: Needs work » Needs review

That's file it against 8.4.x

Status: Needs review » Needs work

The last submitted patch, 18: 2821077-18.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new11.17 KB

Just a reroll

Status: Needs review » Needs work

The last submitted patch, 22: 2821077-22.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new11.1 KB
new1.98 KB

This could fix a lot of the problems ...

wim leers’s picture

Issue tags: +Baltimore2017
StatusFileSize
new6.07 KB
new10.98 KB
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -411,10 +412,20 @@ public function testGet() {
-    $this->assertSame($expected, $actual);
+    $this->assertEquals($expected, $actual);

Let's please keep this as strict as it currently is. We can do the recursive ksort() that e.g. #2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX also does.

All nits I addressed myself. Once the above is fixed, I think this is RTBC.

dawehner’s picture

+1

Who can we ping to ensure this is fine? I think either berdir or tstoeckler would be fine.
Fago would be nice as well, but

wim leers’s picture

Status: Needs review » Needs work

NW for #25.

wim leers’s picture

Issue tags: +Entity Field API

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

wim leers’s picture

dawehner’s picture

Working on this reroll right now

dawehner’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
StatusFileSize
new2.5 KB
new12.72 KB

Status: Needs review » Needs work

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

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new13.88 KB
new1.53 KB

Fixing some test failures ...

Status: Needs review » Needs work

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

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new19.89 KB
new6.02 KB

Let's see whether this fixes more.

wim leers’s picture

@dawehner++

  1. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -460,10 +461,20 @@ public function testGet() {
    +    if ($this->entity instanceof FieldableEntityInterface) {
    +      $expected += [
    +        'rest_test_validation' => [
    +          [
    +            'value' => 'allowed value',
    +          ],
    +        ]
    +      ];
    +    }
    
    @@ -522,6 +533,15 @@ public function testGet() {
    +      if ($this->entity instanceof FieldableEntityInterface) {
    +        $expected += [
    +          'rest_test_validation' => [
    +            [
    +              'value' => 'allowed value',
    +            ],
    +          ]
    +        ];
    +      }
    

    Why was this sufficient in the past (in #25)…

  2. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -562,7 +582,7 @@ public function testGet() {
    -      $this->assertSame($expected, $actual);
    +      $this->assertEquals($expected, $actual);
    

    .

  3. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
    @@ -106,6 +106,11 @@ protected function getExpectedNormalizedEntity() {
    +      'rest_test_validation' => [
    
    +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTestLabel/EntityTestLabelResourceTestBase.php
    @@ -107,6 +107,11 @@ protected function getExpectedNormalizedEntity() {
    +      'rest_test_validation' => [
    
    +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
    @@ -129,6 +129,11 @@ protected function getExpectedNormalizedEntity() {
    +      'rest_test_validation' => [
    
    +++ b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
    @@ -111,6 +111,11 @@ protected function getExpectedNormalizedEntity() {
    +      'rest_test_validation' => [
    

    … and now we need all of these?

    Ideally, we'd be able to avoid this, because it'd causes REST test failures for contrib/custom entity types.

dawehner’s picture

Ideally, we'd be able to avoid this, because it'd causes REST test failures for contrib/custom entity types.

We could check for $this->entity->hasField('rest_test_validation') or so ..., what do you think?

wim leers’s picture

Sounds good!

dawehner’s picture

StatusFileSize
new13.54 KB
new12.26 KB

Let's get this started then.

wim leers’s picture

  1. +++ b/core/modules/rest/tests/modules/rest_test/rest_test.module
    @@ -39,10 +39,12 @@ function rest_test_entity_field_access($operation, FieldDefinitionInterface $fie
    +  if ($entity_type->id() === 'entity_test') {
    

    Oh, interesting, so that means we're now only testing it against \Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestResourceTestBase.

  2. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -474,7 +477,7 @@ public function testGet() {
    -    $this->assertEquals($expected, $actual);
    +    $this->assertSame($expected, $actual);
    

    Yay!

dawehner’s picture

Oh, interesting, so that means we're now only testing it against \Drupal\Tests\rest\Functional\EntityResource\EntityTest\EntityTestResourceTestBase.

Well you either test against all or again one or none :P So you suggest we add this field to all the core entities?

wim leers’s picture

I think testing it with one entity type is sufficient, but the risk is that if we remove the entity_test entity type, we lose this test coverage. That seems extremely unlikely though, so I'm fine with this. I'd also be fine with adding this field to every entity type.

I don't feel very strongly about this.

But the reason I'm surprised is A) it was being added to every entity type before #40, and B)

+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -185,6 +185,10 @@ public function setUp() {
+      if ($this->entity->hasField('rest_test_validation')) {
+        $this->entity->set('rest_test_validation', ['value' => 'allowed value']);
+      }

@@ -460,6 +464,16 @@ public function testGet() {
+    if ($this->entity instanceof FieldableEntityInterface && $this->entity->hasField('rest_test_validation')) {
+      $expected += [
+        'rest_test_validation' => [
+          [
+            'value' => 'allowed value',
+          ],
+        ]
+      ];
+    }

@@ -522,6 +536,15 @@ public function testGet() {
+      if ($this->entity instanceof FieldableEntityInterface && $this->entity->hasField('rest_test_validation')) {
+        $expected += [
+          'rest_test_validation' => [
+            [
+              'value' => 'allowed value',
+            ],
+          ]
+        ];
+      }

@@ -1051,23 +1074,41 @@ public function testPatch() {
+      if ($this->entity->hasField('rest_test_validation')) {

These bits of code suggest it's applied to multiple entity types.

Otherwise, we could just hardcode this to

if (static::$entityTypeId ===
 'entity_test') { …
 }
dawehner’s picture

Otherwise, we could just hardcode this to

I could easily imagine that we want to add this test coverage potentially to more than one entity type, which is why I went with that particular solution.

wim leers’s picture

So that probably means we should go back to doing this for every entity type then?

dawehner’s picture

What about adding it to all core entity types, but ensure that contrib tests not fail?

wim leers’s picture

Sounds good. But isn't it possible to do this for all entity types (contrib or core), and just automatically expand the expected normalization?

+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -411,10 +412,20 @@ public function testGet() {
+    if ($this->entity instanceof FieldableEntityInterface) {
+      $expected += [
+        'rest_test_validation' => [
+          [
+            'value' => 'allowed value',
+          ],
+        ]
+      ];
+    }

@@ -471,6 +482,15 @@ public function testGet() {
+      if ($this->entity instanceof FieldableEntityInterface) {
+        $expected += [
+          'rest_test_validation' => [
+            [
+              'value' => 'allowed value',
+            ],
+          ]
+        ];
+      }

That's what this is doing, and that used to work fine in #25?

I think that should still work fine?

dawehner’s picture

StatusFileSize
new13.87 KB
new3.46 KB

Let's give it a try ...

wim leers’s picture

StatusFileSize
new3.67 KB
new14.77 KB

This looks great! I'd RTBC, but I have a few nits, and one relatively big thing (see point 1), which you will have to approve:

  1. +++ b/core/modules/rest/tests/modules/rest_test/rest_test.module
    @@ -31,3 +33,16 @@ function rest_test_entity_field_access($operation, FieldDefinitionInterface $fie
    +function rest_test_entity_base_field_info(EntityTypeInterface $entity_type) {
    +  $fields = [];
    +  $fields['rest_test_validation'] = BaseFieldDefinition::create('string')
    +    ->setLabel(t('REST test validation field'))
    +    ->setDescription(t('A text field with some special validations attached used for testing purposes'))
    +    ->addConstraint('rest_test_validation');
    +
    +  return $fields;
    +}
    

    I just realized this belongs in \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::setUp(), where we already create the field_rest_test field. Another field for testing purposes.

    (And #2800873: Add XML GET REST test coverage, work around XML encoder quirks is also adding one.)

    So I set out to do this … except I couldn't get it to work :( After extensive debugging, I encountered this gem in the docs of \Drupal\Core\Field\FieldConfigInterface::addConstraint():

       * Note that constraints added via this method are not stored in configuration
       * and as such need to be added at runtime using
       * hook_entity_bundle_field_info_alter().
    

    🤔😭😡🤷‍♂️

    This does not make any sense. The constraint should be stored in the FieldConfig config entity.

    Created #2905890: FieldConfigBase does not store constraints for that.

  2. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -185,6 +185,8 @@ public function setUp() {
    +      $this->entity->set('rest_test_validation', ['value' => 'allowed value']);
    

    i.e. we already have this, might as well create the field there just like the other one!

  3. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -460,6 +462,16 @@ public function testGet() {
    +    }
    +
         static::recursiveKSort($expected);
    
    @@ -557,6 +578,15 @@ public function testGet() {
           $expected = $this->getExpectedNormalizedEntity();
    +
    +      $expected += [
    

    Nit: one unnecessary blank line here.

dawehner’s picture

This does not make any sense. The constraint should be stored in the FieldConfig config entity.

Right, this is exactly what I added it as a base field.

wim leers’s picture

I should've known you knew about these edge cases :) But I suspect you agree with the approach in #49?

wim leers’s picture

Issue tags: +Entity validation
dawehner’s picture

I should've known you knew about these edge cases :) But I suspect you agree with the approach in #49?

For me both approaches are equal.

Maybe one could argue that testing the capability with custom added base fields is actually some level of test coverage which is worth dealing with.

wim leers’s picture

Maybe one could argue that testing the capability with custom added base fields is actually some level of test coverage which is worth dealing with.

Can you explain this a bit more?

dawehner’s picture

Maybe one could argue that testing the capability with custom added base fields is actually some level of test coverage which is worth dealing with.

We have quite some test coverage around entities and their basefields as well as configurable fields. We don't have yet an example of a base field added via a hook in rest API. I know this is a small detail, but you never know, there are infinite ways how something could break.

wim leers’s picture

Status: Needs review » Reviewed & tested by the community
StatusFileSize
new2.88 KB
new1.26 KB
new14.33 KB

Fair point!

Reverting #49.1. That means that compared to @dawehner's last patch in #48, the changes I made are now only whitespace nits (see interdiff-48-55.txt).

RTBC 🎉

wim leers’s picture

Issue summary: View changes

IS updated.

wim leers’s picture

Issue tags: -Needs followup
Related issues: +#2906129: [PP-1] Update ContentEntityBase::validate() to only validate dirty fields
StatusFileSize
new1.14 KB
new14.39 KB

Created the follow-up @dawehner suggested in #12. Removing issue tag. Added to the patch.

tedbow’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs followup, +Needs issue summary update

This review starts from #55 I didn't see patches past that. Will check

  1. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -1051,23 +1097,41 @@ public function testPatch() {
    +        // Change the rest_test_validation field to prove that then its validation
    +        // does run. In subsequent test assertions, it will not be modified, and
    +        // then should not trigger validation errors.
    +        $valid_request_body = $this->getNormalizedPatchEntity() + $this->serializer->normalize($this->entity, static::$format);
    +        $request_options[RequestOptions::BODY] = $this->serializer->serialize($valid_request_body, static::$format);
    +        $response = $this->request('PATCH', $url, $request_options);
    +        $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nrest_test_validation: REST test validation failed\n", $response);
    ...
    +    $valid_request_body = array_diff_key($valid_request_body, ['rest_test_validation' => NULL]);
    

    In this comment we say that we will prove that the validation will run by changing it and setting it to "ALWAYS_FAIL". This does seem to prove that.

    But then we say

    In subsequent test assertions, it will not be modified, and
    then should not trigger validation errors.

    But we change the value of rest_test_validation to NULL.
    So I look at RestTestConstraintValidator and it has

    if (!empty($value[0]['value']) && $value[0]['value'] === 'ALWAYS_FAIL') {
      $this->context->addViolation($constraint->message);
    }
    

    So regardless of the whether rest_test_validation was modified or not the NULL value in the field would never trigger this violation because it is empty, correct?

    So I am not sure what this proves or at least that the comment is correct.

    UPDATE:
    I read this line incorrectly
    $valid_request_body = array_diff_key($valid_request_body, ['rest_test_validation' => NULL]);
    I thought it was setting the field value to NULL when in fact it is removing the rest_test_validation key from the array because we are not all entities will have this field.

    But a few lines before this code we have the comment:

    // 200 for well-formed PATCH request that sends all fields (even including
    // read-only ones, but with unchanged values).
    

    Which is actually not true in the case that the entity has the field rest_test_validation in which case we are not sending all fields because we just removed 1 from the request.

    I think what tripped me up on this is the first comment I mentioned above says:

    In subsequent test assertions, it will not be modified,

    When in fact it is actually not sent so it is confusing to say it is not modified which read to mean sent but has the current value.

    Nit related to my confusion:
    Could we change
    $valid_request_body = array_diff_key($valid_request_body, ['rest_test_validation' => NULL]);
    to
    unset($valid_request_body['rest_test_validation']);
    For me that would be much clear as to what is happening.

  2. Another thing that I find confusing
    protected function validateWithFilteredFields(EntityInterface $entity
    , array $changed_fields) {

    The phpdoc says

       * @param string[] $changed_fields
       *   An array of changed fields. If specified filter out the non applying
    

    But actually this is called as
    $this->validateWithFilteredFields($original_entity, $entity->_restSubmittedFields);

    So when I first saw $changed_fields I assumed since this issue fields that were modified from their current values, probably since this issue deals with "unmodified fields".

    Would it make sense to change $changed_fields to $submitted_fields?

  3. From the issue summary

    For example: node body uses a certain format that user A is not allowed to use, but when user A PATCHes only the node title, they still get a validation error.

    So is this suppose to mean that with this patch user A should be able to send an unchanged body field back in the patch and even if the user doesn't have access to the current format, say full_html the patch should still work?

    If so I don't think this patch fixes the problem. I tried this locally and it does not work.
    I still get

    {
      "message": "Unprocessable Entity: validation failed.\nbody.0.format: The value you selected is not a valid choice.\n"
    }
    

    Or is it suppose to mean that User A can patch the entity has long has they do not send back the body field which does work?

    This at least needs an issue summary update to describe what the desired functionality is.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.11 KB
new13.89 KB

#59

  1. But a few lines before this code we have the comment: […] Which is actually not true […] we are not sending all fields

    Great catch.

    The problem is that the current patch assumes $entity->_restSubmittedFields only contains changed fields, which is not actually the case. This is once again where #2862574: Add ability to track an entity object's dirty fields (and see if it has changed) would come in very handy.

    Fortunately, #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior should allow us to work around this.

    But first, let's remove that line so that we're testing what we should be testing (effectively removing a single line from the patch). This should pass tests in theory, but in practice it will fail, due to the use of $entity->_restSubmittedFields. Thanks, eagle-eyed Ted! 🦅

  2. Hah, this ties in to my analysis for point 1! We definitely need $changed_fields.
  3. No, what you're describing is field access. Field access checking happens before validation. Fields that you're not allowed to edit you still won't be allowed to edit.

    OTOH, you're saying that you're not modifying anything. Well, if you're not modifying anything, then #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior should already be ensuring that it works fine:

          // Check access for all received fields, but only if they are being
          // changed. The bundle of an entity, for example, must be provided for
          // denormalization to succeed, but it may not be changed.
          elseif (!$original_field->equals($field) && !$original_field->access('edit')) {
            throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
          }
    

    So I'm not sure what's going on there?

P.S.: why Needs followup?

wim leers’s picture

StatusFileSize
new3.82 KB
new14.8 KB

Turns out there were two things wrong:

  1. The use of $entity->_restSubmittedfields as I already identified in #60.
  2. The updated test coverage was first setting ALWAYS_FAIL, then doing a PATCH request and expecting it to fail. But if it's already set to that value, then it should not fail! So the order was simply wrong.

So, less complicated than I feared :)

wim leers’s picture

StatusFileSize
new3.6 KB
new14.72 KB

Two nits that I noticed while doing this:

  1. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -226,6 +226,7 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    +    $changed_fields = [];
    

    Needs a @todo to remove this.

  2. +++ b/core/modules/rest/tests/modules/rest_test/rest_test.module
    @@ -31,3 +33,16 @@ function rest_test_entity_field_access($operation, FieldDefinitionInterface $fie
    +function rest_test_entity_base_field_info(EntityTypeInterface $entity_type) {
    +  $fields = [];
    +  $fields['rest_test_validation'] = BaseFieldDefinition::create('string')
    

    This runs unconditionally, so…

  3. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -1051,18 +1078,36 @@ public function testPatch() {
    +      if ($this->entity->hasField('rest_test_validation')) {
    

    … we can also remove this condition.

The last submitted patch, 60: 2821077-60.patch, failed testing. View results

tedbow’s picture

  1. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -1093,19 +1093,20 @@ public function testPatch() {
    +        // Set the rest_test_validation field to always fail validation, which
    +        // allows asserting that not modifying that field does not trigger
    +        // validation errors.
    +        $this->entity->set('rest_test_validation', 'ALWAYS_FAIL');
    +        $this->entity->save();
    

    Because we moved this $this->entity->save() to after the expected failed patch request and we removed this line if the previous patch

    $valid_request_body = array_diff_key($valid_request_body, ['rest_test_validation' => NULL]);
    

    Then now we are truly testing sending back an unmodfied field that would fail on validation if validation were called! 🙌

    This address the type of the problem described in the issue summary regarding a format for the body field.

    I tested this manually and now it works! 🎉

  2. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -247,11 +248,17 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    +      // Determine which fields actually changed, i.e. which ones should be
    +      // set & validated.
    +      if (!$original_field->equals($field)) {
    +        $changed_fields[] = $field_name;
    +        $original_entity->set($field_name, $field->getValue());
    +      }
         }
     
         // Validate the received data before saving.
    -    $this->validateWithFilteredFields($original_entity, $entity->_restSubmittedFields);
    +    $this->validateWithFilteredFields($original_entity, $changed_fields);
    

    This looks good so we validate but filter unchanged fields! So the test should pass!

wim leers’s picture

So:

  • no need for IS update?
  • no need for followup?
  • back to RTBC?
tedbow’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs followup

Yeah follow up was mistake because I had the form up open when you removed the tag.

Added a single sentence to the Issue Summary to described desired behavior.

RTBC 😃

amateescu’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
@@ -23,6 +24,42 @@
   protected function validate(EntityInterface $entity) {

Instead of splitting this method into 4 other methods, wouldn't it be simpler to simply add a new $filter_fields = [] parameter which, if provided, would call filterByFields() on the violations list?

wim leers’s picture

#67 Thanks for the review! You got me wondering why we were doing it this seemingly convoluted way, because what you suggest indeed sounds much simpler!

Fun fact: what you suggest is exactly what the patch in #4 did! But based on @tstoeckler's feedback in #9.2, @dawehner ended up with the current approach in #10.

We could easily go back on this, but what you suggest is what @tstoeckler found confusing in #9.

I don't have a strong opinion. Whatever you and @tstoeckler agree upon is fine by me.

dawehner’s picture

Instead of splitting this method into 4 other methods, wouldn't it be simpler to simply add a new $filter_fields = [] parameter which, if provided, would call filterByFields() on the violations list?

One reason I went with the 4 methods is that their intensions are really clear by their name.

amateescu’s picture

StatusFileSize
new4.34 KB

Reading #9.2 again, @tstoeckler actually wanted just better documentation for the new $changed_fields parameter, to make it clear that passing an empty array means that no filtering is done on the violations list.

In addition to that, #9.3 was not really addressed properly with the answer from #10:

As far as I understand its used in the form to be able to separate the violations which have to apply to certain widgets from other errors.

@dawehner based his initial code and the reply from #10 on the code from \Drupal\Core\Entity\ContentEntityForm::flagViolations(), but that code is actually just a helper method for deciding who is responsible for converting entity validation errors to form errors. The form itself is responsible for reporting entity-level violations and field widget are responsible for reporting field-level violations.

Instead, we should base REST's code on \Drupal\Core\Entity\ContentEntityForm::validateForm(), which gets the full list of violations and then filters it by field access and fields that were actually changed by the form.

This means that we can simplify the validateWithFilteredFields() method to this:

  protected function validateWithFilteredFields(EntityInterface $entity, array $changed_fields) {
    if ($violations = $this->doValidation($entity)) {
      $violations->filterByFields(array_diff(array_keys($entity->getFieldDefinitions()), $changed_fields));
      $this->processViolations($violations);
    }
  }

Note that I also removed the instanceof check because doValidation() already has it. Now.. doesn't this look exactly like validate() with an added filterByFields() call? It sure does to me, which made me wonder if we really need a different method for this. My answer was "no", so if we merge validateWithFilteredFields() with validate(), then the new doValidation() doesn't make sense on its own, so we can inline it as well in the parent validate() method.

This brings us to only two methods, validate() and processViolations(). Now, the doxygen block from validate() says that it throws an UnprocessableEntityHttpException if validation errors are found, but it's actually processViolations() the one who throws that exception (and doesn't document it in the docs), so, in my mind, there's no need for that additional call to a different method just to throw an exception that should've been thrown by the parent call.

In conclusion, I think the code from #4 was way better than the changes from #10, and @tstoeckler concerns could easily be addressed by documenting the new $changed_fields param better.

The patch from #62 does not apply to HEAD anymore because #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior was reverted, but here's an interdiff for it which brings us back to one method and also addresses @tstoeckler's review from #9.

dawehner’s picture

I'm trying a reroll now and then address @amateescu latest patch ...
@amateescu I like how your interdiff simplifies things ...

dawehner’s picture

Title: PATCHing entities validates the entire entity, also unmodified fields, so unmodified fields can throw validation errors » [pp-1] PATCHing entities validates the entire entity, also unmodified fields, so unmodified fields can throw validation errors
Status: Needs review » Postponed

I would actually suggest to postpone this on #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior given the test code conflicts quite heavily ...

wim leers’s picture

Title: [pp-1] PATCHing entities validates the entire entity, also unmodified fields, so unmodified fields can throw validation errors » [PP-1] PATCHing entities validates the entire entity, also unmodified fields, so unmodified fields can throw validation errors

#70++ Thank you! ❤️
#71: I completely agree!
#72: :( This is exactly what I feared that the revert of #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior would cause :(

wim leers’s picture

Title: [PP-1] PATCHing entities validates the entire entity, also unmodified fields, so unmodified fields can throw validation errors » PATCHing entities validates the entire entity, also unmodified fields, so unmodified fields can throw validation errors
Status: Postponed » Active
wim leers’s picture

Status: Active » Needs review
StatusFileSize
new14.72 KB

Straight rebase #62. Had to resolve some conflicts. The only actual change in implementation is in EntityResource, because #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior changed that pretty significantly. Fortunately, the logic is still identical.

wim leers’s picture

StatusFileSize
new2.98 KB
new13 KB

I also noticed this opportunity for simplification, which also makes this patch a bit smaller (because more pure additions rather than changing existing lines of code)

wim leers’s picture

StatusFileSize
new4.53 KB
new12.24 KB

And now applied @amateescu's #70 interdiff. @dawehner said in #71 he liked how it simplifies things, and I can only wholeheartedly agree with that! :)

It even makes the patch smaller!

@amateescu++

Status: Needs review » Needs work

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

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new11.39 KB
new3.23 KB

Okay, so going from #76 to #77 by applying @amateescu's #70 interdiff causes more failures. Interesting.

But, #75/#76 also contain failures, unlike the last green patch they're equivalent to, in #62. Let's fix those failures first. Most of the failures seem to be in XML test coverage. XML test coverage didn't exist yet when #62 was green, which explains it.
Making this new rest_test_validation field that this patch is adding behave the same as the field_rest_test or field_rest_test_multivalue fields solves that.

(Note: we should probably still rename it to field_rest_test_validation for consistency with the other test fields.)

wim leers’s picture

StatusFileSize
new1.58 KB
new11.47 KB

The changes in #79 prevented the PATCH request body from actually containing a value for the validation test field. Enforce this.

wim leers’s picture

StatusFileSize
new1.02 KB
new11.44 KB

And then finally: there is one subtle change that @amateescu's #70 introduces: it calls ->filterByFields() if $changed_fields is not empty. But we're specifically testing the case where we do send values without them being different. So $changed_fields = [], which means that subtle change in #70 ends up causing unmodified fields to still throw validation errors. But only if there are zero unmodified fields.

The solution is simple.

(This one was pretty tricky to debug — I spent at least 30 minutes pulling my hair over this :D)

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

Status: Needs review » Needs work

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

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

wim leers’s picture

@gabesullice just pointed out how similar this is to #2968972: Cannot PATCH an entity with dangling references in an ER field in JSON API; it's essentially the same solution!

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new11.46 KB

Also, let's get this going again. Rebased #81; one file has been moved in the past 5 months.

Status: Needs review » Needs work

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

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.13 KB
new11.55 KB
wim leers’s picture

StatusFileSize
new851 bytes
new10.77 KB

#88 should make all failing testPost() tests pass again.

This should make the tests involving EntityTest entities pass again. (There was an obsolete leftover — see #41+#42 and related comments.)

tstoeckler’s picture

So ::checkPatchFieldAccess() contains this:

    // If the user is allowed to edit the field, it is always safe to set the
    // received value. We may be setting an unchanged value, but that is ok.
    if ($original_field->access('edit')) {
      return TRUE;
    }

I feel like we should just change that to:

if ($original_field->access('edit') && !$original_field->equals($field)) {

(and adapt the comment accordingly. Then we can save the if in the calling code. That will mean we will no longer call ->set() on field items that are equal, but we already decided in #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior that that's not a behavior change because ->set() is not a no-op for equal fields that's a bug.

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

wim leers’s picture

StatusFileSize
new1.13 KB
new11.84 KB

I think #89 will be down to one fail, this should bring it to zero!

wim leers’s picture

StatusFileSize
new830 bytes
new12.02 KB

Yay, a @tstoeckler review! 🎉 😀 Thanks, Tobias! :)

My first reaction is: I do not want to touch that code if at all possible, because it took months to get #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior (which contained it) to land!

Second reaction: I should take a closer look nonetheless. [… does that …] Yep, it's still about changing code that was extremely tricky to get just right.

I have an alternative idea:

+    // If no fields are changed, we can send a response immediately!
+    if (empty($changed_fields)) {
+      return new ModifiedResourceResponse($original_entity, 200);
+    }

That's even less work! Otherwise we're still triggering the validating and saving of something that we already know to be the same.

What do you think?

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

tstoeckler’s picture

Issue tags: +Needs tests

I was about to write a lengthy comment on the reasoning for #90 but in the process I realized one thing. #90 was just about suggesting a different "placement" of the code with no functional change relative to the patch.

However, I am now convinced that the patch itself contains a funtional bug. Given how the #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior I would not be surprised for @cburschka (or anyone else, for that matter) to prove me wrong on this, but for now I am convinced nonetheless 😉.

The issue summary reads:

If a REST request sends a field that the user has access to but is unchanged from the current field value the validation should not be run on this field.

The sublety lies with the word "access". Per #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior and given that a user does not have access to edit a given field we do not allow submitting that field even if it is unchanged if the user does not have view access on it. As discussed at length in that issue this prevents information disclosure as otherwise the user could just submit a bunch of different values to this field and as soon as they would not get a 403 for a response they would have found out the current field value - in effect having "viewed" it.

This issue is now about running field validation, but I think a similar logic applies there, as well, in a sort of "reverse" way. If a user has "edit" access to a field, but not "view" access, the current patch would not run field validation of the field value matches the current way. That way the resulting response depends (or at least may depend) on the current value which in turn may allow the user to figure the field value.

It is a bit far-fetched - but not impossible - to conjure up an actual "exploit" with this. In particular, if the current value of a field is invalid for whatever reason. This is (not normally the case, but) entirely possible because while forms and Rest do perform validation by default, it is not enforced when saving an entity manually and even through forms there can be situations where not all fields are valid. An attacker (that - again - has edit but not view access to this field) could then try to PATCH all kinds of invalid values to this field and if they hit the current one, a 200 response would be returned, which would neatly inform them of the current (invalid) field value, in effect having granted them view access.

So I think we actually need to check view access before excluding fields from validation because they are unchanged. Luckily, this will not be a very invasive change. In fact, we can just switch the order of the two if-hunks in ::checkPatchFieldAccess() (and I guess we will have to update the respective comments a bit). Then we can just remove the if that the current patch adds entirely and just populate $changed_fields iff we call ::set().

Tagging "Needs tests" because I think we want something along the lines of the above "attack scenario" so that we do not regress here.

The idea in #93 is actually quite neat, I think, but I don't understand how it's related to the suggestion in #90, to be honest, and it also doesn't fix the above. That doesn't mean we shouldn't add it, though, in fact I really see reason not to.

wim leers’s picture

Assigned: Unassigned » wim leers
Issue tags: +Security improvements

the current patch would not run field validation of the field value matches the current way.

It is a bit far-fetched - but not impossible - to conjure up an actual "exploit" with this.

🙉🙈😱

Well-spotted!

wim leers’s picture

Issue tags: -Needs tests
StatusFileSize
new1.2 KB
new12.47 KB

Fortunately, the test coverage already being added by this patch made it trivial to test this :)

This should fail! Instead of the expected 422 (to not disclose the current value), we get a 200 (which does disclose the current value).

wim leers’s picture

StatusFileSize
new2.29 KB
new13.65 KB

And the fix!

tstoeckler’s picture

Wow, thanks for the quick turnaround! Looks really good, only minor points.

  1. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -238,12 +240,18 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    +    if (empty($changed_fields)) {
    

    Could just be if($changed_fields)

  2. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
    @@ -14,16 +15,20 @@
    +   *   list to include only this set of fields. Defaults to an empty array,
    +   *   which means that all violations will be reported.
    

    I know this comment is there because I explicitly requested it, but looking at the code now again, I'm not actually sure it is true?! If $changed_fields is empty, all fields will be passed to filterByFields() which means that no (field-level) violations will be reported at all, unless I'm mistaken.

  3. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -1151,6 +1153,36 @@ public function testPatch() {
    +      // does run. In subsequent test assertions, it will not be modified, and
    +      // then should not trigger validation errors.
    

    Seems this is no longer true?

  4. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -1151,6 +1153,36 @@ public function testPatch() {
    +      $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nrest_test_validation: REST test validation failed\n", $response);
    +
    +    }
    

    Unnecessary empty line.

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

wim leers’s picture

StatusFileSize
new3.32 KB
new13.85 KB
  1. I prefer this explicitness.
  2. Well-spotted. filterByFields() is confusing. 😅 I also added logic now to prevent that nonsensical case, which is possible thanks to #93!
  3. It is, but the text is ambiguous since #97. Improved, which should fix this :)
  4. ✔️
tstoeckler’s picture

Thanks, looks great to me. Still needs an issue summary update, but I wouldn't RTBC regardless, because another pair of eyes wouldn't hurt.

wim leers’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

#59.3 tagged this as needing an IS update. I cannot reproduce the problem in #59.3. The bit it cited in the IS as being problematic is actually correct. #64.1 confirmed this. :) @tedbow even updated the IS in #66! He just forgot to remove the tag :p

That being said, since then, quite a few changes happened. So, updated the IS for those.

tedbow’s picture

Status: Needs review » Needs work

Needs work to remove the todo.

The other point may just be me forgetting the details of this issue.

  1. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -229,6 +229,8 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    +    // @todo Remove $changed_fields in https://www.drupal.org/node/2906129.
    +    $changed_fields = [];
    

    #2906129: [PP-1] Update ContentEntityBase::validate() to only validate dirty fields issue was closed as "Works as designed" by @amateescu. So we should probably remove this @todo.

  2. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -1151,6 +1153,39 @@ public function testPatch() {
    +      // Information disclosure prevented: when a malicious user correctly
    +      // guesses the current invalid value of a field, ensure a 200 is not sent
    +      // because this would disclose to the attacker what the current value is.
    +      // @see rest_test_entity_field_access()
    +      $response = $this->request('PATCH', $url, $request_options);
    +      $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nrest_test_validation: REST test validation failed\n", $response);
    

    If the user doesn't have access to view the field should return a 422 response code but not return the actual message. Would there ever be a case where the validation message gives away the current value?

    For instance what if in content_moderation you have transitions
    draft -> publish
    publish -> draft

    but not
    publish -> publish

    i.e. you can't actually make any changes to entity without take it out of published status.

    Would the message "published to publish not a valid transition" give away the current value.?

    We want to run validation to prevent disclosure but for the user without access to view the field what is value of displaying the actually validate message to that user?

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new874 bytes
new13.87 KB
  1. Good catch! d.o makes it hard to be notified of closed issues. Updated the @todo; if #2906129: [PP-1] Update ContentEntityBase::validate() to only validate dirty fields won't happen then #2862574: Add ability to track an entity object's dirty fields (and see if it has changed) will.
  2. Excellent question! If the user is not allowed to view nor edit it, then they won't get a 422. They'll get a 403. The potential information disclosure vulnerability @tstoeckler pointed out is for this extreme edge case: An attacker (that - again - has edit but not view access to this field) .The test coverage you quoted is for that very use case: a field that the current user can edit but not view!
    You're talking about the use case where the user can neither edit nor view. HEAD already has explicit test coverage for that:
        // DX: 403 when entity contains field without 'edit' nor 'view' access, even
        // when the value for that field matches the current value. This is allowed
        // in principle, but leads to information disclosure.
        $response = $this->request('PATCH', $url, $request_options);
        $this->assertResourceErrorResponse(403, "Access denied on updating field 'field_rest_test'.", $response);
    

    (Added in #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior.)

tedbow’s picture

Assigned: wim leers » Unassigned
Status: Needs review » Reviewed & tested by the community

@Wim Leers thanks for the explanation. Re-reading the issue and patch that looks good!

🎉RTBC!🎉

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
@@ -14,16 +15,19 @@
+   * @param string[] $changed_fields
+   *   (optional) An array of field names. If specified, filters the violations
+   *   list to include only this set of fields.

@@ -33,6 +37,15 @@ protected function validate(EntityInterface $entity) {
+    // Filter violations by the changed fields. New entities only have changed
+    // fields.
+    if (!$entity->isNew()) {
+      if (empty($changed_fields)) {
+        throw new \LogicException("Saving an entity without changing any fields does not make sense.");
+      }
+      $violations->filterByFields(array_diff(array_keys($entity->getFieldDefinitions()), $changed_fields));
+    }

I think this name could be improved. I.e. be $fields_to_validate As the point here is that this is a list of fields to validate. And we should document the default behaviour is to validate all fields.

Also I'm concerned about BC since I think not providing the value should continue with todays behaviour of validating all the fields. So I think the exception thrown here might be an API break.

tedbow’s picture

Status: Needs work » Needs review
StatusFileSize
new3.56 KB
new12.06 KB

@alexpott I think these changes sound good.

  1. I changed the name to $fields_to_validate.
  2. Because to validate() these is just generic list of field names now I removed the logic of not calling \Drupal\Core\Entity\EntityConstraintViolationListInterface::filterByFields() if the entity is new. This logic only made sense if these were "changed" fields. This should not make a difference because the only place this method is being called with a value for this optional parameter is \Drupal\rest\Plugin\rest\resource\EntityResource::patch() and because it is PATCH request this will never be new entity.
  3. And we should document the default behaviour is to validate all fields.

    The documentation of $fields_to_validate provides documentation now but I added more documentation for the method.

    But I also added a bit about fields the users has access to since the default behavior is not to validate all fields.

  4. Also I'm concerned about BC since I think not providing the value should continue with todays behaviour of validating all the fields.

    I have remove this exception. I don't think it will cause any test fails but will see.

    I could see where this could be a BC break. If you created a decouple entity entity form.

    1. User loads the edit form
    2. User hits save without making any changes
    3. Decouple app makes a PATCH request with unchanged entity fields
    4. Currently there would no error
    5. Decouple app displays "You entity saved"

    If we threw an exception here that would be a BC break that app would not know how to handle.
    Also maybe the user was just saving the form with no changes to update "changed" field(or another computed field) to get the entity at the top of a list ordered by that field. So it actually could make sense to save with no changes.

wim leers’s picture

Status: Needs review » Reviewed & tested by the community

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

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed 73769ef5f8 to 8.7.x and 55ffe4fa1f to 8.6.x. Thanks!

Backporting to 8.6.x as an important bug fix for API first. AFAICS the changes here are totally BC compatible and the behaviour changes should only benefit real sites.

  • alexpott committed 73769ef on 8.7.x
    Issue #2821077 by Wim Leers, dawehner, tedbow, amateescu, tstoeckler,...

  • alexpott committed 55ffe4f on 8.6.x
    Issue #2821077 by Wim Leers, dawehner, tedbow, amateescu, tstoeckler,...
wim leers’s picture

Status: Fixed » Closed (fixed)

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