Issue #1866908 by klausi: Honor entity and field access control in REST Services.
parent
5818a99bfc
commit
f969d2c104
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue