Problem/Motivation

Working on #2291055: REST resources for anonymous users: register I have detected that I cannot create user entities. When I try it the response is {"error":"Access denied on creating field pass"}.

Proposed resolution

As @Berdir proposed at #2291055: REST resources for anonymous users: register the usage of create here is not correct.

    foreach ($entity as $field_name => $field) {
      if (!$field->access('create')) {
        throw new AccessDeniedHttpException(String::format('Access denied on creating field @field', array('@field' => $field_name)));
      }
    }

"CreateTest" covers node and entity_test but we should cover the user entity too.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

marthinal’s picture

A quick update. It's not possible to use edit for "changed" field.

ChangedFieldItemList::defaultAccess

  /**
   * {@inheritdoc}
   */
  public function defaultAccess($operation = 'view', AccountInterface $account = NULL) {
    // It is not possible to edit the changed field.
    return AccessResult::allowedIf($operation !== 'edit');
  }
}
Berdir’s picture

That is correct, it is not possible to write to changed.

marthinal’s picture

As discussed in IRC with @Berdir, "changed" never should be checked. We only need to check fields where we actually post some data.

marthinal’s picture

FileSize
2.8 KB

For 'patch' we are only checking fields added to the hal+json. As @Berdir suggested me, we can use _restPatchFields and probably it's good idea to rename it.

Let's try then!

marthinal’s picture

Status: Needs work » Needs review
Berdir’s picture

+++ b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php
@@ -174,7 +174,7 @@ public function denormalize($data, $class, $format = NULL, array $context = arra
     if (isset($context['request_method']) && $context['request_method'] == 'patch') {
-      $entity->_restPatchFields = array_keys($data);
+      $entity->_restSubmittedFields = array_keys($data);
     }

You need to remove the request_method check then, because a create will be a post, not a patch.

This check used to be necessary when we were messing with the entity object in weird ways. We no longer do, which means we can remove the check, and decide in EntityResource when and how to use it.

marthinal’s picture

FileSize
898 bytes
2.91 KB

Sure! Thanks @Berdir :)

The last submitted patch, 4: 2405091-4.patch, failed testing.

The last submitted patch, 4: 2405091-4.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 7: 2405091-7.patch, failed testing.

Berdir’s picture

+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -87,8 +87,8 @@ public function post(EntityInterface $entity = NULL) {
-    foreach ($entity as $field_name => $field) {
-      if (!$field->access('create')) {
+    foreach ($entity->_restSubmittedFields as $field_name => $field) {
+      if (!$field->access('edit')) {

_restSubmittedFields is just a list of field names, which you need to call as $entity->get($field_name)->access()

marthinal’s picture

Status: Needs work » Needs review
FileSize
1014 bytes
3.06 KB

Thanks @Berdir!

So let's try this!

Status: Needs review » Needs work

The last submitted patch, 12: 2405091-12.patch, failed testing.

marthinal’s picture

Assigned: Unassigned » marthinal
Issue tags: +Needs tests

Looks like we have access problems creating a node "Response body: {"error":"Access denied on creating field status"}"

Berdir’s picture

status requires administer nodes permission I think, see fieldAccess() in NodeAccessControlHandler. Try to remove that or give the user that permission.

Berdir’s picture

Priority: Major » Critical
Issue tags: +Security

Also, this is a security issue, because it means we have no/invalid access handling for creating new content.

marthinal’s picture

Status: Needs work » Needs review
FileSize
972 bytes
4.01 KB

I prefer to create the content from a non-admin user... Anyways we can test both but I'm not sure if we need it...

catch’s picture

Issue tags: +Triaged D8 critical

Yes this is bad per #16, marking as triaged.

Berdir’s picture

Status: Needs review » Needs work
+++ b/core/modules/rest/src/Tests/CreateTest.php
@@ -41,6 +41,16 @@ public function testCreate() {
       $entity = entity_create($entity_type, $entity_values);
+      ¶
+      // These fields can only be added if we have "administer content" permission.

Trailing spaces here.

Yes, I think it would be useful to have a second test as a user with administer nodes permission, to ensure that we can then write those.

And we need a third test, to verify that it is not allowed to write e.g. the node status as a normal user. One field as an example should be enough to verify that the glue code between rest and field access is working.

And of course a test for creating a user :)

marthinal’s picture

Issue tags: +SprintWeekend2015
clemens.tolboom’s picture

The issue summary states

but we should cover the user entity too

How is this patch accomplish that.

+++ b/core/modules/rest/src/Tests/CreateTest.php
@@ -41,6 +41,16 @@ public function testCreate() {
+      ¶

whitespace

marthinal’s picture

Status: Needs work » Needs review
FileSize
12.73 KB
15.79 KB

Test that creates entities as non-admin user + admin user + covering user entity creation.

Berdir’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -87,9 +87,9 @@ public function post(EntityInterface $entity = NULL) {
    +    foreach ($entity->_restSubmittedFields as $key => $field_name) {
    +      if (!$entity->get($field_name)->access('edit')) {
    +        throw new AccessDeniedHttpException(String::format('Access denied on creating field @field', array('@field' => $field_name)));
           }
    

    Let's add a comment here, that explains that we only want to check edit permissions for fields that were actually submitted by the user. Maybe also mention that field access makes no difference between between create and update, so the edit operation is used here.

  2. +++ b/core/modules/rest/src/Tests/CreateTest.php
    @@ -28,88 +28,126 @@ class CreateTest extends RESTTestBase {
       public function testCreate() {
         $serializer = $this->container->get('serializer');
    -    $entity_types = array('entity_test', 'node');
    

    This test is getting pretty hard to read, with all those different cases. Maybe we can find a different way to structure it, a separate method per entity type and some helper methods for he parts that are actually the same?

  3. +++ b/core/modules/rest/src/Tests/RESTTestBase.php
    @@ -351,4 +351,21 @@ protected function loadEntityFromLocationHeader($location_url) {
    +  /**
    +   * Remove nod fields where a non-admin user cannot write to.
    +   *
    +   * @param $entity
    +   */
    +  protected function removeNodeFieldsForNonAdminUsers($entity) {
    

    As this is specific to nodes, maybe we should name it $node and type hint on NodeInterface.

    unset() on entity fields is a bit strange, I suggest you use $entity->status = NULL or maybe array() instead, if that works. Maybe check with @yched.

marthinal’s picture

Status: Needs work » Needs review
FileSize
21.21 KB

Missing user entity test. It's commented at this moment (public function _testCreateUser()). I can only create user entities as admin using basic auth and not sure why... the last test at #22 passed with the same code. Once this test is fixed then I need to add the only test .patch

marthinal’s picture

Sorry @Berdir I forgot to explain a little bit better :)

1) Done.

2) Done.

3) Done

Missing to remove the unset().

larowlan’s picture

+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -87,9 +87,12 @@ public function post(EntityInterface $entity = NULL) {
+    // Only want to check 'edit' permissions for fields that were actually submitted by the user.
+    // Field access makes no difference between 'create' and 'update', so the 'edit' operation is used here.

>80 chars

Will try and take a look at the user tests later in the week

marthinal’s picture

FileSize
21.21 KB
1.7 KB

Let's check the test results for testCreateUser().

Status: Needs review » Needs work

The last submitted patch, 27: 2405091-27.patch, failed testing.

marthinal’s picture

From my Rest Client I have the same problem for the user entity creation and I can fix the problem using the token (X-CSRF-Token request header). For the node creation the result should be the same but I'm not sure why the test works as expected in this case...

marthinal’s picture

The test fails because a user with only 'administer users' permission cannot create user entities from rest. From the UI I can create user entities with the same user... The admin user(uid 1) can create user entities from rest.

And here is the problem (EntityResource::post) :


    if (!$entity->access('create')) {
      throw new AccessDeniedHttpException();
    }

Maybe I'm missing something...

larowlan’s picture

Issue tags: +Critical Office Hours

Tagging in the hope someone comes to critical office hours interested in this

Berdir’s picture

Status: Needs work » Needs review
FileSize
21.22 KB
2.81 KB

Awesome, you do keep finding bugs :)

This is as simple as stupid.

The user entity annotation has:

*   admin_permission = "administer user",

That is now how this permission is called :)

That said, there were also a few things wrong in the test. You checked that access is denied for non-administer users, but then still continued to call createEntityOverRestApi(), which obviously failed :)

Also, createEntityWithoutProperPermissions() did not work for users (because there *are* users), and it is not necessary, so removed that.

The test is certainly already better to understand, still thinking about how to improve it. Not sure yet. Also don't want to delay fixing those critical bugs because of imperfect tests :) That said, comments and coding standards (docblocks, comment lengths and such) certainly need some work.

marthinal’s picture

Status: Needs review » Needs work

oh I see! Didn't know about this change. :)

Sure! I will work later today to improve the comments at least.

marthinal’s picture

Status: Needs work » Needs review
FileSize
14.06 KB
24.51 KB

Ok let's try it :)

I have a couple of questions:

1) Not sure if I should use '\Drupal::entityManager()->getStorage($entity_type)->create($values)' instead of entity_create();

2) it is very helpful to add this comment about CSRF token but not sure if we want it here :)

    // POST request to create the current entity. GET request for CSRF token
    // is included into the httpRequest() method.
marthinal’s picture

About 1), some functions are deprecated but probably in this case makes no sense at all :)

Berdir’s picture

+++ b/core/modules/rest/src/Tests/CreateTest.php
@@ -276,17 +329,24 @@ public function createEntityNoData($entity_type) {
    * @param $entity
    * @param $entity_type
    * @param array $context
    */
   public function createEntityInvalidSerialized($entity, $entity_type, $context = array()) {

Adding more comments is good, but what I meant is coding standards for e.g. @param, with a type and description, correctly type hint on EntityInterface and so on.

1. Yes, I suggest you use the storage handler if you don't know the entity class (e.g. Node::create() works too, but only if you know that you are working wit nodes).

marthinal’s picture

Improving comments and coding standards. Using storage handler when I don't know the entity class and using the static create() method when I know the entity class.

The last submitted patch, 37: 2405091-37-only-test.patch, failed testing.

RavindraSingh’s picture

FileSize
26.72 KB

Seems good to me, Just adding t() for notices.

dawehner’s picture

  1. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -87,9 +87,13 @@ public function post(EntityInterface $entity = NULL) {
    -        throw new AccessDeniedHttpException(String::format('Access denied on creating field ', array('@field' => $field_name)));
    ...
    +        throw new AccessDeniedHttpException(String::format(t('Access denied on creating field @field'), array('@field' => $field_name)));
    

    -1 to use t() in exceptions IMHO

  2. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -87,9 +87,13 @@ public function post(EntityInterface $entity = NULL) {
    +    foreach ($entity->_restSubmittedFields as $key => $field_name) {
    
    @@ -97,7 +101,7 @@ public function post(EntityInterface $entity = NULL) {
    -      $this->logger->notice('Created entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
    +      $this->logger->notice(t('Created entity %type with ID %id.'), array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
    

    ... We don't translate logging messages

  3. +++ b/core/modules/rest/src/Tests/CreateTest.php
    @@ -24,103 +28,361 @@ class CreateTest extends RESTTestBase {
    +    //$this->assertFalse(entity_load_multiple('entity_test', NULL, TRUE), 'No entity has been created in the database.');
    

    Alright, let's get rid of this line :)

  4. +++ b/core/modules/rest/src/Tests/RESTTestBase.php
    @@ -351,4 +353,22 @@ protected function loadEntityFromLocationHeader($location_url) {
    +    unset($node->status);
    +    unset($node->created);
    +    unset($node->changed);
    +    unset($node->promote);
    +    unset($node->sticky);
    +    unset($node->revision_timestamp);
    +    unset($node->uid);
    +
    +    return $node;
    

    It feels like a hack / implementation detail, that this is even possible.

marthinal’s picture

@RavindraSingh Looks like we don't need any translation here.

@dawhener

3) Done.

4) Not sure when exactly this method could break something... I was reviewing how the magic method __unset() works and looks ok. Also I don't know the best way to remove these fields then. I think a method is probably the best way to avoid repeating the same code ...

dawehner’s picture

@RavindraSingh Looks like we don't need any translation here.

Its not about needing, its about making things translated which shouldn't, as things are broken, so t() causes even potentially harm.

mikey_p’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -87,9 +87,13 @@ public function post(EntityInterface $entity = NULL) {
    +    // submitted by the user.Field access makes no difference between 'create'
    

    missing space after user.

  2. +++ b/core/modules/rest/src/Tests/CreateTest.php
    @@ -24,103 +28,358 @@ class CreateTest extends RESTTestBase {
    +      $this->createEntityOverRestApi($entity_type, $serialized);
    

    Might want to rename or change the accepted parameters. It's not clear why this isn't user later where it's using httpRequest(). Maybe something like:

    
    public function assertCreateEntityResponse($entity_type, $serialized = NULL, $response = 201) {
    
    
  3. +++ b/core/modules/rest/src/Tests/CreateTest.php
    @@ -24,103 +28,358 @@ class CreateTest extends RESTTestBase {
    +      $this->readEntityIdFromHeaderAndDb($entity_type, $entity, $entity_values);
    

    Might want to rename this make it clear that this method deletes the entity.

  4. +++ b/core/modules/rest/src/Tests/CreateTest.php
    @@ -24,103 +28,358 @@ class CreateTest extends RESTTestBase {
    +      // Try to send invalid data that cannot be correctly deserialized.
    +      $this->createEntityInvalidData($entity_type);
    +
    +      // Try to send no data at all, which does not make sense on POST requests.
    +      $this->createEntityNoData($entity_type);
    +
    +      // Try to send invalid data to trigger the entity validation constraints. Send a UUID that is too long.
    +      $this->createEntityInvalidSerialized($entity, $entity_type);
    +
    +      // Try to create an entity without proper permissions.
    +      $this->createEntityWithoutProperPermissions($entity_type, $serialized, ['account' => $account]);
    +
    

    Not sure if this is too pedantic, but I would expect any helper method that has assertions in it to start with assert, i.e. assertCreateEntityInvalidData(). Is there a core convention for this?

marthinal’s picture

Status: Needs work » Needs review
FileSize
8.42 KB
25.93 KB

1) Done.

2) Not sure we need it...

3) We are removing a loaded entity from db.

    // Get the entity using the ID found.
    $loaded_entity = \Drupal::entityManager()->getStorage($entity_type)->load($id);

    // Delete the entity loaded from DB.
    $loaded_entity->delete();

4) Done. I'm not sure it's 100% correct but makes sense to me. Anyways needs review.

Thanks for reviewing the issue!!!

mikey_p’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs reroll
git ac https://www.drupal.org/files/issues/2405091-44.patch
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 26548  100 26548    0     0   158k      0 --:--:-- --:--:-- --:--:--  227k
error: patch failed: core/modules/rest/src/Tests/RESTTestBase.php:67
error: core/modules/rest/src/Tests/RESTTestBase.php: patch does not apply
catch’s picture

Overall looks good. Found some nitpicks.

  1. +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
    @@ -87,9 +87,13 @@ public function post(EntityInterface $entity = NULL) {
    +    // Only want to check 'edit' permissions for fields that were actually
    

    Can skip 'want to'.

  2. +++ b/core/modules/rest/src/Tests/CreateTest.php
    @@ -24,103 +28,358 @@ class CreateTest extends RESTTestBase {
    +
    +    // @todo Remove the user reference field for now until deserialization for
    +    // entity references is implemented.
    +    unset($entity_values['user_id']);
    +
    

    Is there an issue for this?

  3. +++ b/core/modules/rest/src/Tests/CreateTest.php
    @@ -24,103 +28,358 @@ class CreateTest extends RESTTestBase {
    +  /**
    +   * Try to send invalid data to trigger the entity validation constraints.
    +   * Send a UUID that is too long.
    +   *
    

    Should be one line.

  4. +++ b/core/modules/rest/src/Tests/RESTTestBase.php
    @@ -351,4 +353,22 @@ protected function loadEntityFromLocationHeader($location_url) {
    +   * Remove node fields where a non-admin user cannot write to.
    

    s/where/that/

    Or I'd reword to 'Remove node fields that can only be written by an admin user'.

Berdir’s picture

2. That comment is just moved in the patch. However, it looks weird to me, we supported entity reference fields for a very, very long time? maybe we can just remove that unset() ?

marthinal’s picture

Status: Needs work » Needs review
FileSize
2.62 KB
25.42 KB

#47

1) Done.

2) Done.

3) Done.

4) Done.

Thanks!

Berdir’s picture

  1. +++ b/core/modules/rest/src/Tests/CreateTest.php
    @@ -341,8 +337,7 @@ public function assertCreateEntityNoData($entity_type) {
       /**
    -   * Try to send invalid data to trigger the entity validation constraints.
    -   * Send a UUID that is too long.
    +   * Try to send invalid data to trigger the entity validation constraints. Send a UUID that is too long.
    

    Neither version is correct.

    The first sentence must not be more than 80 characters.

    Two ways to fix it:

    1. Keep the old version, but add an empty line inbetween.
    2. Rewrite into a single, shorter sentence. I'd prefer that. Something like "Send an invalid UUID to trigger the entity validation." ?

  2. +++ b/core/modules/rest/src/Tests/CreateTest.php
    @@ -341,8 +337,7 @@ public function assertCreateEntityNoData($entity_type) {
        * @param EntityInterface $entity
        *   The entity we want to check that was inserted correctly.
    

    classes/interfaces in @param/@return should always use the full namespace, so \Drupal\Core\Entity\EntityInterface.

  3. +++ b/core/modules/rest/src/Tests/RESTTestBase.php
    @@ -348,7 +348,7 @@ protected function loadEntityFromLocationHeader($location_url) {
        * @param NodeInterface $node
        * @return NodeInterface
    

    Also namespace missing and missing description for @param and possibly @return (not visible from context)

marthinal’s picture

@Berdir Done!

Berdir’s picture

Status: Needs review » Needs work
Issue tags: -Needs tests, -Needs reroll
+++ b/core/modules/rest/src/Tests/RESTTestBase.php
@@ -69,10 +69,13 @@ protected function setUp() {
+   *
+   * @return boolean
+   *   The content returned from the call to curl_exec().

Not sure if we need to mention curl_exec() here, I'd just say "... from the request." ? Also, this should be a string, not a boolean? (which would be "bool").

I agree that the unset business is spooky. I think it would be better to set them to NULL/empty array, that should have the same effect. But that unset stuff seems to be an existing thing in rest test, so probably better to clean it up in a separate issue, this is big enough and solves the critical part.

marthinal’s picture

Status: Needs work » Needs review
FileSize
1.29 KB
26.13 KB

@Berdir

Yes, it was a string, sorry.

Berdir’s picture

Status: Needs review » Reviewed & tested by the community

Thanks, back to RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This issue addresses a critical bug and is allowed per https://www.drupal.org/core/beta-changes. Committed 466759d and pushed to 8.0.x. Thanks!

  • alexpott committed 466759d on 8.0.x
    Issue #2405091 by marthinal, Berdir, RavindraSingh: Cannot create user...
catch’s picture

I'd started reviewing this but only found comments over 80 chars. Opened #2418559: Fix comment formatting in /core/modules/rest/src/Tests/CreateTest.php as very minor follow-up.

marthinal’s picture

Opened a new one(#2418587: Set entity values to NULL instead of using unset() method: unset() is misleading) to assign NULL instead of using unset(). (#52)

Status: Fixed » Closed (fixed)

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