From a567be3ee03182a7ef0982db9ca8ef548296779e Mon Sep 17 00:00:00 2001 From: Dries Date: Tue, 13 Aug 2013 14:57:21 -0400 Subject: [PATCH] Issue #2054187 by juampy: Restrict authentication provider for REST resources. --- core/modules/file/file.module | 9 ++ .../FileUsage/DatabaseFileUsageBackend.php | 16 ++- .../file/FileUsage/FileUsageInterface.php | 9 ++ .../file/lib/Drupal/file/Tests/UsageTest.php | 38 +++++++ core/modules/rest/config/rest.settings.yml | 10 ++ .../rest/EventSubscriber/RouteSubscriber.php | 5 + .../rest/lib/Drupal/rest/Tests/AuthTest.php | 107 ++++++++++++++++++ .../lib/Drupal/rest/Tests/RESTTestBase.php | 8 +- 8 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php diff --git a/core/modules/file/file.module b/core/modules/file/file.module index ff37a175bc5..e9352defd52 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -38,6 +38,15 @@ function file_help($path, $arg) { } } +/** + * Implements hook_modules_uninstalled(). + */ +function file_modules_uninstalled($modules) { + foreach ($modules as $module) { + file_usage()->deleteByModule($module); + } +} + /** * Implements hook_menu(). */ diff --git a/core/modules/file/lib/Drupal/file/FileUsage/DatabaseFileUsageBackend.php b/core/modules/file/lib/Drupal/file/FileUsage/DatabaseFileUsageBackend.php index 80163c4f047..5b9b4175fc6 100644 --- a/core/modules/file/lib/Drupal/file/FileUsage/DatabaseFileUsageBackend.php +++ b/core/modules/file/lib/Drupal/file/FileUsage/DatabaseFileUsageBackend.php @@ -46,7 +46,7 @@ class DatabaseFileUsageBackend extends FileUsageBase { } /** - * Implements Drupal\file\FileUsage\FileUsageInterface::add(). + * {@inheritdoc} */ public function add(File $file, $module, $type, $id, $count = 1) { $this->connection->merge($this->tableName) @@ -64,7 +64,7 @@ class DatabaseFileUsageBackend extends FileUsageBase { } /** - * Implements Drupal\file\FileUsage\FileUsageInterface::delete(). + * {@inheritdoc} */ public function delete(File $file, $module, $type = NULL, $id = NULL, $count = 1) { // Delete rows that have a exact or less value to prevent empty rows. @@ -99,7 +99,7 @@ class DatabaseFileUsageBackend extends FileUsageBase { } /** - * Implements Drupal\file\FileUsage\FileUsageInterface::listUsage(). + * {@inheritdoc} */ public function listUsage(File $file) { $result = $this->connection->select($this->tableName, 'f') @@ -113,4 +113,14 @@ class DatabaseFileUsageBackend extends FileUsageBase { } return $references; } + + /** + * {@inheritdoc} + */ + public function deleteByModule($module) { + $this->connection->delete($this->tableName) + ->condition('module', $module) + ->execute(); + } + } diff --git a/core/modules/file/lib/Drupal/file/FileUsage/FileUsageInterface.php b/core/modules/file/lib/Drupal/file/FileUsage/FileUsageInterface.php index d9a248dd68c..1e06dbb94e5 100644 --- a/core/modules/file/lib/Drupal/file/FileUsage/FileUsageInterface.php +++ b/core/modules/file/lib/Drupal/file/FileUsage/FileUsageInterface.php @@ -72,4 +72,13 @@ interface FileUsageInterface { * */ public function listUsage(File $file); + + /** + * Removes all records for a specific module; useful for uninstalling modules. + * + * @param string $module + * The name of the module using files. + */ + public function deleteByModule($module); + } diff --git a/core/modules/file/lib/Drupal/file/Tests/UsageTest.php b/core/modules/file/lib/Drupal/file/Tests/UsageTest.php index 17e51d867a5..7b15045b773 100644 --- a/core/modules/file/lib/Drupal/file/Tests/UsageTest.php +++ b/core/modules/file/lib/Drupal/file/Tests/UsageTest.php @@ -52,6 +52,44 @@ class UsageTest extends FileManagedTestBase { $this->assertEqual($usage['testing']['bar'][2], 2, 'Returned the correct count.'); } + /** + * Tests file_usage()->deleteByModule(). + */ + function testDeleteByModule() { + $file = $this->createFile(); + db_insert('file_usage') + ->fields(array( + 'fid' => $file->id(), + 'module' => 'testing', + 'type' => 'foo', + 'id' => 1, + 'count' => 1 + )) + ->execute(); + db_insert('file_usage') + ->fields(array( + 'fid' => $file->id(), + 'module' => 'testing', + 'type' => 'bar', + 'id' => 2, + 'count' => 2 + )) + ->execute(); + db_insert('file_usage') + ->fields(array( + 'fid' => $file->id(), + 'module' => 'file', + 'type' => 'bar', + 'id' => 2, + 'count' => 2 + )) + ->execute(); + + file_usage()->deleteByModule('testing'); + + $this->assertIdentical(array('file' => array('bar' => array(2 => '2'))), file_usage()->listUsage($file), 'All recors for the "testing" module have been deleted.'); + } + /** * Tests file_usage()->add(). */ diff --git a/core/modules/rest/config/rest.settings.yml b/core/modules/rest/config/rest.settings.yml index 2780a6d9b78..91c9a7bf6d6 100644 --- a/core/modules/rest/config/rest.settings.yml +++ b/core/modules/rest/config/rest.settings.yml @@ -14,3 +14,13 @@ resources: # GET: # supported_formats: # - json +# +# To enable only specific authentication methods for an operation, list them +# at supported_auth. +# For example, the following config only allows Basic HTTP authenticated +# requests for the POST method on the node entity. +# resources: +# entity:node: +# POST: +# supported_auth: +# - http_basic diff --git a/core/modules/rest/lib/Drupal/rest/EventSubscriber/RouteSubscriber.php b/core/modules/rest/lib/Drupal/rest/EventSubscriber/RouteSubscriber.php index b1bcb7c80dc..4b9f9fbfafb 100644 --- a/core/modules/rest/lib/Drupal/rest/EventSubscriber/RouteSubscriber.php +++ b/core/modules/rest/lib/Drupal/rest/EventSubscriber/RouteSubscriber.php @@ -72,6 +72,11 @@ class RouteSubscriber implements EventSubscriberInterface { $collection->add("rest.$name", $route); continue; } + // Check if there are authentication provider restrictions in the + // configuration and apply them to the route. + if (is_array($enabled_methods[$method]['supported_auth']) && !empty($enabled_methods[$method]['supported_auth'])) { + $route->setOption('_auth', $enabled_methods[$method]['supported_auth']); + } // If there is no format requirement or if it matches the // configuration also add the route. $format_requirement = $route->getRequirement('_format'); diff --git a/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php b/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php new file mode 100644 index 00000000000..6e637c4a572 --- /dev/null +++ b/core/modules/rest/lib/Drupal/rest/Tests/AuthTest.php @@ -0,0 +1,107 @@ + 'Resource authentication', + 'description' => 'Tests authentication provider restrictions.', + 'group' => 'REST', + ); + } + + /** + * Tests reading from an authenticated resource. + */ + public function testRead() { + $entity_type = 'entity_test'; + + // Enable a test resource through GET method and basic HTTP authentication. + $this->enableService('entity:' . $entity_type, 'GET', NULL, array('http_basic')); + + // Create an entity programmatically. + $entity = $this->entityCreate($entity_type); + $entity->save(); + + // Try to read the resource as an anonymous user, which should not work. + $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType); + $this->assertResponse('401', 'HTTP response code is 401 when the request is not authenticated and the user is anonymous.'); + $this->assertText('A fatal error occurred: No authentication credentials provided.'); + + // Create a user account that has the required permissions to read + // resources via the REST API, but the request is authenticated + // with session cookies. + $permissions = $this->entityPermissions($entity_type, 'view'); + $permissions[] = 'restful get entity:' . $entity_type; + $account = $this->drupalCreateUser($permissions); + $this->drupalLogin($account); + + // Try to read the resource with session cookie authentication, which is + // not enabled and should not work. + $response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType); + $this->assertResponse('403', 'HTTP response code is 403 when the request is authenticated but not authorized.'); + $this->drupalLogout(); + + // Now read it with the Basic authentication which is enabled and should + // work. + $response = $this->basicAuthGet('entity/' . $entity_type . '/' . $entity->id(), $account->getUsername(), $account->pass_raw); + $this->assertResponse('200', 'HTTP response code is 200 for successfuly authorized requests.'); + $this->curlClose(); + } + + /** + * Performs a HTTP request with Basic authentication. + * + * We do not use \Drupal\simpletest\WebTestBase::drupalGet because we need to + * set curl settings for basic authentication. + * + * @param string $path + * The request path. + * @param string $username + * The user name to authenticate with. + * @param string $password + * The password. + * + * @return string + * Curl output. + */ + protected function basicAuthGet($path, $username, $password) { + $out = $this->curlExec( + array( + CURLOPT_HTTPGET => TRUE, + CURLOPT_URL => url($path, array('absolute' => TRUE)), + CURLOPT_NOBODY => FALSE, + CURLOPT_HTTPAUTH => CURLAUTH_BASIC, + CURLOPT_USERPWD => $username . ':' . $password, + ) + ); + + $this->verbose('GET request to: ' . $path . + '
' . $out); + + return $out; + } + +} diff --git a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php index 7f5417794ba..94d3e61de0b 100644 --- a/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php +++ b/core/modules/rest/lib/Drupal/rest/Tests/RESTTestBase.php @@ -192,8 +192,10 @@ abstract class RESTTestBase extends WebTestBase { * The HTTP method to enable, e.g. GET, POST etc. * @param string $format * (Optional) The serialization format, e.g. hal_json. + * @param array $auth + * (Optional) The list of valid authentication methods. */ - protected function enableService($resource_type, $method = 'GET', $format = NULL) { + protected function enableService($resource_type, $method = 'GET', $format = NULL, $auth = array()) { // Enable REST API for this entity type. $config = config('rest.settings'); $settings = array(); @@ -205,6 +207,10 @@ abstract class RESTTestBase extends WebTestBase { $settings[$resource_type][$method] = array(); } } + if (is_array($auth) && !empty($auth)) { + $settings[$resource_type][$method]['supported_auth'] = $auth; + } + $config->set('resources', $settings); $config->save();