diff --git a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php index a2e49e387d8..f96f1143d76 100644 --- a/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php +++ b/core/modules/jsonld/lib/Drupal/jsonld/JsonldEntityWrapper.php @@ -111,6 +111,7 @@ class JsonldEntityWrapper { public function getProperties() { // Properties to skip. $skip = array('id'); + $properties = array(); // Create language map property structure. foreach ($this->entity->getTranslationLanguages() as $langcode => $language) { diff --git a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php index 47d1b0b2aab..77bcc00c40f 100644 --- a/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/lib/Drupal/rest/Plugin/rest/resource/EntityResource.php @@ -13,6 +13,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageException; use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -44,6 +45,14 @@ class EntityResource extends ResourceBase { $definition = $this->getDefinition(); $entity = entity_load($definition['entity_type'], $id); if ($entity) { + if (!$entity->access('view')) { + throw new AccessDeniedHttpException(); + } + foreach ($entity as $field_name => $field) { + if (!$field->access('view')) { + unset($entity->{$field_name}); + } + } return new ResourceResponse($entity); } throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id))); @@ -63,6 +72,9 @@ class EntityResource extends ResourceBase { * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ public function post($id, EntityInterface $entity) { + if (!$entity->access('create')) { + throw new AccessDeniedHttpException(); + } $definition = $this->getDefinition(); // Verify that the deserialized entity is of the type that we expect to // prevent security issues. @@ -74,6 +86,11 @@ class EntityResource extends ResourceBase { if (!$entity->isNew()) { throw new BadRequestHttpException(t('Only new entities can be created')); } + foreach ($entity as $field_name => $field) { + if (!$field->access('create')) { + throw new AccessDeniedHttpException(t('Access denied on creating field @field.', array('@field' => $field_name))); + } + } try { $entity->save(); watchdog('rest', 'Created entity %type with ID %id.', array('%type' => $entity->entityType(), '%id' => $entity->id())); @@ -114,10 +131,27 @@ class EntityResource extends ResourceBase { if ($original_entity == FALSE) { throw new NotFoundHttpException(); } + if (!$original_entity->access('update')) { + throw new AccessDeniedHttpException(); + } + $info = $original_entity->entityInfo(); + // Make sure that the entity ID is the one provided in the URL. + $entity->{$info['entity_keys']['id']} = $id; + // Overwrite the received properties. - foreach ($entity->getProperties() as $name => $property) { - if (isset($entity->{$name})) { - $original_entity->{$name} = $property; + foreach ($entity as $field_name => $field) { + if (isset($entity->{$field_name})) { + if (empty($entity->{$field_name})) { + if (!$original_entity->{$field_name}->access('delete')) { + throw new AccessDeniedHttpException(t('Access denied on deleting field @field.', array('@field' => $field_name))); + } + } + else { + if (!$original_entity->{$field_name}->access('update')) { + throw new AccessDeniedHttpException(t('Access denied on updating field @field.', array('@field' => $field_name))); + } + } + $original_entity->{$field_name} = $field; } } try { @@ -147,6 +181,9 @@ class EntityResource extends ResourceBase { $definition = $this->getDefinition(); $entity = entity_load($definition['entity_type'], $id); if ($entity) { + if (!$entity->access('delete')) { + throw new AccessDeniedHttpException(); + } try { $entity->delete(); watchdog('rest', 'Deleted entity %type with ID %id.', array('%type' => $entity->entityType(), '%id' => $entity->id())); @@ -160,18 +197,4 @@ class EntityResource extends ResourceBase { } throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id))); } - - /** - * Overrides ResourceBase::permissions(). - */ - public function permissions() { - $permissions = parent::permissions(); - // Mark all items as administrative permissions for now. - // @todo Remove this restriction once proper entity access control is - // implemented. See http://drupal.org/node/1866908 - foreach ($permissions as $name => $permission) { - $permissions[$name]['restrict access'] = TRUE; - } - return $permissions; - } } diff --git a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php index d6973e57130..32784a8d71d 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/CreateTest.php @@ -41,7 +41,9 @@ class CreateTest extends RESTTestBase { $this->enableService('entity:' . $entity_type, 'POST'); // Create a user account that has the required permissions to create // resources via the REST API. - $account = $this->drupalCreateUser(array('restful post entity:' . $entity_type)); + $permissions = $this->entityPermissions($entity_type, 'create'); + $permissions[] = 'restful post entity:' . $entity_type; + $account = $this->drupalCreateUser($permissions); $this->drupalLogin($account); $entity_values = $this->entityValues($entity_type); @@ -70,6 +72,18 @@ class CreateTest extends RESTTestBase { $loaded_entity->delete(); + // Try to create an entity with an access protected field. + // @see entity_test_entity_field_access() + $entity->field_test_text->value = 'no access value'; + $serialized = $serializer->serialize($entity, 'drupal_jsonld'); + $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, 'application/vnd.drupal.ld+json'); + $this->assertResponse(403); + $this->assertFalse(entity_load_multiple($entity_type, NULL, TRUE), 'No entity has been created in the database.'); + + // Restore the valid test value. + $entity->field_test_text->value = $entity_values['field_test_text'][0]['value']; + $serialized = $serializer->serialize($entity, 'drupal_jsonld'); + // Try to send invalid data that cannot be correctly deserialized. $this->httpRequest('entity/' . $entity_type, 'POST', 'kaboom!', 'application/vnd.drupal.ld+json'); $this->assertResponse(400); diff --git a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php index 2bed8bae965..7f72ee7662d 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/DeleteTest.php @@ -34,12 +34,16 @@ class DeleteTest extends RESTTestBase { */ public function testDelete() { // Define the entity types we want to test. - $entity_types = array('entity_test', 'node', 'user'); + // @todo expand this test to at least nodes and users once their access + // controllers are implemented. + $entity_types = array('entity_test'); foreach ($entity_types as $entity_type) { $this->enableService('entity:' . $entity_type, 'DELETE'); // Create a user account that has the required permissions to delete // resources via the REST API. - $account = $this->drupalCreateUser(array('restful delete entity:' . $entity_type)); + $permissions = $this->entityPermissions($entity_type, 'delete'); + $permissions[] = 'restful delete entity:' . $entity_type; + $account = $this->drupalCreateUser($permissions); $this->drupalLogin($account); // Create an entity programmatically. diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php index 6cbfbd82b06..cd9bc22ea4b 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php @@ -218,4 +218,29 @@ abstract class RESTTestBase extends WebTestBase { } parent::drupalLogin($user); } + + /** + * Provides the necessary user permissions for entity operations. + * + * @param string $entity_type + * The entity type. + * @param type $operation + * The operation, one of 'view', 'create', 'update' or 'delete'. + * + * @return array + * The set of user permission strings. + */ + protected function entityPermissions($entity_type, $operation) { + switch ($entity_type) { + case 'entity_test': + switch ($operation) { + case 'view': + return array('view test entity'); + case 'create': + case 'update': + case 'delete': + return array('administer entity_test content'); + } + } + } } diff --git a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php index e157a2d26d5..f3ccca73fbf 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/ReadTest.php @@ -39,9 +39,11 @@ class ReadTest extends RESTTestBase { $entity_types = array('entity_test'); foreach ($entity_types as $entity_type) { $this->enableService('entity:' . $entity_type, 'GET'); - // Create a user account that has the required permissions to delete + // Create a user account that has the required permissions to read // resources via the REST API. - $account = $this->drupalCreateUser(array('restful get entity:' . $entity_type)); + $permissions = $this->entityPermissions($entity_type, 'view'); + $permissions[] = 'restful get entity:' . $entity_type; + $account = $this->drupalCreateUser($permissions); $this->drupalLogin($account); // Create an entity programmatically. @@ -66,6 +68,17 @@ class ReadTest extends RESTTestBase { $decoded = drupal_json_decode($response); $this->assertEqual($decoded['error'], 'Entity with ID 9999 not found', 'Response message is correct.'); + // Make sure that field level access works and that the according field is + // not available in the response. + // @see entity_test_entity_field_access() + $entity->field_test_text->value = 'no access value'; + $entity->save(); + $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/vnd.drupal.ld+json'); + $this->assertResponse(200); + $this->assertHeader('content-type', 'application/vnd.drupal.ld+json'); + $data = drupal_json_decode($response); + $this->assertFalse(isset($data['field_test_text']), 'Field access protexted field is not visible in the response.'); + // Try to read an entity without proper permissions. $this->drupalLogout(); $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/vnd.drupal.ld+json'); diff --git a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php index 31c6461a863..44ea0635d3e 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/UpdateTest.php @@ -41,7 +41,9 @@ class UpdateTest extends RESTTestBase { $this->enableService('entity:' . $entity_type, 'PATCH'); // Create a user account that has the required permissions to create // resources via the REST API. - $account = $this->drupalCreateUser(array('restful patch entity:' . $entity_type)); + $permissions = $this->entityPermissions($entity_type, 'update'); + $permissions[] = 'restful patch entity:' . $entity_type; + $account = $this->drupalCreateUser($permissions); $this->drupalLogin($account); // Create an entity and save it to the database. @@ -76,6 +78,32 @@ class UpdateTest extends RESTTestBase { $entity = entity_load($entity_type, $entity->id(), TRUE); $this->assertNull($entity->field_test_text->value, 'Test field has been cleared.'); + // Enable access protection for the text field. + // @see entity_test_entity_field_access() + $entity->field_test_text->value = 'no access value'; + $entity->save(); + + // Try to empty a field that is access protected. + $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json'); + $this->assertResponse(403); + + // Re-load the entity from the database. + $entity = entity_load($entity_type, $entity->id(), TRUE); + $this->assertEqual($entity->field_test_text->value, 'no access value', 'Text field was not updated.'); + + // Try to update an access protected field. + $serialized = $serializer->serialize($patch_entity, 'drupal_jsonld'); + $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, 'application/vnd.drupal.ld+json'); + $this->assertResponse(403); + + // Re-load the entity from the database. + $entity = entity_load($entity_type, $entity->id(), TRUE); + $this->assertEqual($entity->field_test_text->value, 'no access value', 'Text field was not updated.'); + + // Restore the valid test value. + $entity->field_test_text->value = $this->randomString(); + $entity->save(); + // Try to update a non-existing entity with ID 9999. $this->httpRequest('entity/' . $entity_type . '/9999', 'PATCH', $serialized, 'application/vnd.drupal.ld+json'); $this->assertResponse(404); diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module index 9720399a734..80f1b3945fd 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.module +++ b/core/modules/system/tests/modules/entity_test/entity_test.module @@ -289,8 +289,13 @@ function entity_test_entity_view_mode_info() { * @see \Drupal\system\Tests\Entity\FieldAccessTest::testFieldAccess() */ function entity_test_entity_field_access($operation, $field, $account) { - if ($field->getName() == 'field_test_text' && $field->value == 'no access value') { - return FALSE; + if ($field->getName() == 'field_test_text') { + if ($field->value == 'no access value') { + return FALSE; + } + elseif ($operation == 'delete' && $field->value == 'no delete access value') { + return FALSE; + } } }