Commit f969d2c1 authored by Dries's avatar Dries
Browse files

Issue #1866908 by klausi: Honor entity and field access control in REST Services.

parent 5818a99b
......@@ -111,6 +111,7 @@ public function getTypeUri() {
public function getProperties() {
// Properties to skip.
$skip = array('id');
$properties = array();
// Create language map property structure.
foreach ($this->entity->getTranslationLanguages() as $langcode => $language) {
......
......@@ -13,6 +13,7 @@
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 @@ public function get($id) {
$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 @@ public function get($id) {
* @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 @@ public function post($id, EntityInterface $entity) {
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 @@ public function patch($id, EntityInterface $entity) {
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 @@ public function delete($id) {
$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 @@ public function delete($id) {
}
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;
}
}
......@@ -41,7 +41,9 @@ public function testCreate() {
$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 @@ public function testCreate() {
$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);
......
......@@ -34,12 +34,16 @@ public static function getInfo() {
*/
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.
......
......@@ -218,4 +218,29 @@ protected function drupalLogin($user) {
}
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');
}
}
}
}
......@@ -39,9 +39,11 @@ public function testRead() {
$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 @@ public function testRead() {
$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');
......
......@@ -41,7 +41,9 @@ public function testPatchUpdate() {
$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 @@ public function testPatchUpdate() {
$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);
......
......@@ -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;
}
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment