Commit 3a13f946 authored by effulgentsia's avatar effulgentsia

Issue #2721595 by Wim Leers, dawehner: Simplify REST configuration

parent df1fe181
......@@ -7,25 +7,14 @@ dependencies:
- node
id: entity.node
plugin_id: 'entity:node'
granularity: method
granularity: resource
configuration:
GET:
supported_formats:
- hal_json
supported_auth:
- basic_auth
POST:
supported_formats:
- hal_json
supported_auth:
- basic_auth
PATCH:
supported_formats:
- hal_json
supported_auth:
- basic_auth
DELETE:
supported_formats:
- hal_json
supported_auth:
- basic_auth
methods:
- GET
- POST
- PATCH
- DELETE
formats:
- hal_json
authentication:
- basic_auth
......@@ -8,7 +8,6 @@ rest.settings:
label: 'Domain of the relation'
# Method-level granularity of REST resource configuration.
# @todo Add resource-level granularity in https://www.drupal.org/node/2721595.
rest_resource.method:
type: mapping
mapping:
......@@ -25,6 +24,29 @@ rest_resource.method:
type: rest_request
label: 'DELETE method settings'
# Resource-level granularity of REST resource configuration.
rest_resource.resource:
type: mapping
mapping:
methods:
type: sequence
label: 'Supported methods'
sequence:
type: string
label: 'HTTP method'
formats:
type: sequence
label: 'Supported formats'
sequence:
type: string
label: 'Format'
authentication:
type: sequence
label: 'Supported authentication providers'
sequence:
type: string
label: 'Authentication provider'
rest_request:
type: mapping
mapping:
......
......@@ -30,6 +30,8 @@ function rest_requirements($phase) {
/**
* Install the REST config entity type and fix old settings-based config.
*
* @see rest_post_update_create_rest_resource_config_entities()
*/
function rest_update_8201() {
\Drupal::entityDefinitionUpdateManager()->installEntityType(\Drupal::entityTypeManager()->getDefinition('rest_resource_config'));
......
......@@ -16,11 +16,8 @@
/**
* Create REST resource configuration entities.
*
* @todo https://www.drupal.org/node/2721595 Automatically upgrade those REST
* resource config entities that have the same formats/auth mechanisms for all
* methods to "granular: resource".
*
* @see rest_update_8201()
* @see https://www.drupal.org/node/2308745
*/
function rest_post_update_create_rest_resource_config_entities() {
$resources = \Drupal::state()->get('rest_update_8201_resources', []);
......@@ -34,6 +31,43 @@ function rest_post_update_create_rest_resource_config_entities() {
}
}
/**
* Simplify method-granularity REST resource config to resource-granularity.
*
* @see https://www.drupal.org/node/2721595
*/
function rest_post_update_resource_granularity() {
/** @var \Drupal\rest\RestResourceConfigInterface[] $resource_config_entities */
$resource_config_entities = RestResourceConfig::loadMultiple();
foreach ($resource_config_entities as $resource_config_entity) {
if ($resource_config_entity->get('granularity') === RestResourceConfigInterface::METHOD_GRANULARITY) {
$configuration = $resource_config_entity->get('configuration');
$format_and_auth_configuration = [];
foreach (array_keys($configuration) as $method) {
$format_and_auth_configuration['format'][$method] = implode(',', $configuration[$method]['supported_formats']);
$format_and_auth_configuration['auth'][$method] = implode(',', $configuration[$method]['supported_auth']);
}
// If each method has the same formats and the same authentication
// providers configured, convert it to 'granularity: resource', which has
// a simpler/less verbose configuration.
if (count(array_unique($format_and_auth_configuration['format'])) === 1 && count(array_unique($format_and_auth_configuration['auth'])) === 1) {
$first_method = array_keys($configuration)[0];
$resource_config_entity->set('configuration', [
'methods' => array_keys($configuration),
'formats' => $configuration[$first_method]['supported_formats'],
'authentication' => $configuration[$first_method]['supported_auth']
]);
$resource_config_entity->set('granularity', RestResourceConfigInterface::RESOURCE_GRANULARITY);
$resource_config_entity->save();
}
}
}
}
/**
* @} End of "addtogroup updates-8.1.x-to-8.2.x".
*/
......@@ -61,31 +61,25 @@ public static function create(ContainerInterface $container) {
*/
public function calculateDependencies(RestResourceConfigInterface $rest_config) {
$granularity = $rest_config->get('granularity');
if ($granularity === RestResourceConfigInterface::METHOD_GRANULARITY) {
return $this->calculateDependenciesForMethodGranularity($rest_config);
}
else {
throw new \InvalidArgumentException("A different granularity then 'method' is not supported yet.");
// @todo Add resource-level granularity support in https://www.drupal.org/node/2721595.
// Dependency calculation is the same for either granularity, the most
// notable difference is that for the 'resource' granularity, the same
// authentication providers and formats are supported for every method.
switch ($granularity) {
case RestResourceConfigInterface::METHOD_GRANULARITY:
$methods = $rest_config->getMethods();
break;
case RestResourceConfigInterface::RESOURCE_GRANULARITY:
$methods = array_slice($rest_config->getMethods(), 0, 1);
break;
default:
throw new \InvalidArgumentException('Invalid granularity specified.');
}
}
/**
* Calculates dependencies of a specific rest resource configuration.
*
* @param \Drupal\rest\RestResourceConfigInterface $rest_config
* The rest configuration.
*
* @return string[][]
* Dependencies keyed by dependency type.
*
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies()
*/
protected function calculateDependenciesForMethodGranularity(RestResourceConfigInterface $rest_config) {
// The dependency lists for authentication providers and formats
// generated on container build.
$dependencies = [];
foreach (array_keys($rest_config->get('configuration')) as $request_method) {
foreach ($methods as $request_method) {
// Add dependencies based on the supported authentication providers.
foreach ($rest_config->getAuthenticationProviders($request_method) as $auth) {
if (isset($this->authProviders[$auth])) {
......@@ -102,6 +96,10 @@ protected function calculateDependenciesForMethodGranularity(RestResourceConfigI
}
}
if (isset($dependencies['module'])) {
sort($dependencies['module']);
}
return $dependencies;
}
......@@ -121,12 +119,13 @@ protected function calculateDependenciesForMethodGranularity(RestResourceConfigI
*/
public function onDependencyRemoval(RestResourceConfigInterface $rest_config, array $dependencies) {
$granularity = $rest_config->get('granularity');
if ($granularity === RestResourceConfigInterface::METHOD_GRANULARITY) {
return $this->onDependencyRemovalForMethodGranularity($rest_config, $dependencies);
}
else {
throw new \InvalidArgumentException("A different granularity then 'method' is not supported yet.");
// @todo Add resource-level granularity support in https://www.drupal.org/node/2721595.
switch ($granularity) {
case RestResourceConfigInterface::METHOD_GRANULARITY:
return $this->onDependencyRemovalForMethodGranularity($rest_config, $dependencies);
case RestResourceConfigInterface::RESOURCE_GRANULARITY:
return $this->onDependencyRemovalForResourceGranularity($rest_config, $dependencies);
default:
throw new \InvalidArgumentException('Invalid granularity specified.');
}
}
......@@ -183,7 +182,71 @@ protected function onDependencyRemovalForMethodGranularity(RestResourceConfigInt
}
}
}
if (!empty($configuration_before != $configuration)) {
if ($configuration_before != $configuration && !empty($configuration)) {
$rest_config->set('configuration', $configuration);
// Only mark the dependencies problems as fixed if there is any
// configuration left.
$changed = TRUE;
}
}
// If the dependency problems are not marked as fixed at this point they
// should be related to the resource plugin and the config entity should
// be deleted.
return $changed;
}
/**
* Informs the entity that entities it depends on will be deleted.
*
* @param \Drupal\rest\RestResourceConfigInterface $rest_config
* The rest configuration.
* @param array $dependencies
* An array of dependencies that will be deleted keyed by dependency type.
* Dependency types are, for example, entity, module and theme.
*
* @return bool
* TRUE if the entity has been changed as a result, FALSE if not.
*/
public function onDependencyRemovalForResourceGranularity(RestResourceConfigInterface $rest_config, array $dependencies) {
$changed = FALSE;
// Only module-related dependencies can be fixed. All other types of
// dependencies cannot, because they were not generated based on supported
// authentication providers or formats.
if (isset($dependencies['module'])) {
// Try to fix dependencies.
$removed_auth = array_keys(array_intersect($this->authProviders, $dependencies['module']));
$removed_formats = array_keys(array_intersect($this->formatProviders, $dependencies['module']));
$configuration_before = $configuration = $rest_config->get('configuration');
if (!empty($removed_auth) || !empty($removed_formats)) {
// Try to fix dependency problems by removing affected
// authentication providers and formats.
foreach ($removed_formats as $format) {
if (in_array($format, $rest_config->getFormats('GET'))) {
$configuration['formats'] = array_diff($configuration['formats'], $removed_formats);
}
}
foreach ($removed_auth as $auth) {
if (in_array($auth, $rest_config->getAuthenticationProviders('GET'))) {
$configuration['authentication'] = array_diff($configuration['authentication'], $removed_auth);
}
}
if (empty($configuration['authentication'])) {
// Remove the key if there are no more authentication providers
// supported.
unset($configuration['authentication']);
}
if (empty($configuration['formats'])) {
// Remove the key if there are no more formats supported.
unset($configuration['formats']);
}
if (empty($configuration['authentication']) || empty($configuration['formats'])) {
// If there no longer are any supported authentication providers or
// formats, this REST resource can no longer function, and so we
// cannot fix this config entity to keep it working.
$configuration = [];
}
}
if ($configuration_before != $configuration && !empty($configuration)) {
$rest_config->set('configuration', $configuration);
// Only mark the dependencies problems as fixed if there is any
// configuration left.
......
......@@ -45,7 +45,9 @@ class RestResourceConfig extends ConfigEntityBase implements RestResourceConfigI
/**
* The REST resource configuration granularity.
*
* @todo Currently only 'method', but https://www.drupal.org/node/2721595 will add 'resource'
* Currently either:
* - \Drupal\rest\RestResourceConfigInterface::METHOD_GRANULARITY
* - \Drupal\rest\RestResourceConfigInterface::RESOURCE_GRANULARITY
*
* @var string
*/
......@@ -112,12 +114,13 @@ public function getResourcePlugin() {
* {@inheritdoc}
*/
public function getMethods() {
if ($this->granularity === RestResourceConfigInterface::METHOD_GRANULARITY) {
return $this->getMethodsForMethodGranularity();
}
else {
throw new \InvalidArgumentException("A different granularity then 'method' is not supported yet.");
// @todo Add resource-level granularity support in https://www.drupal.org/node/2721595.
switch ($this->granularity) {
case RestResourceConfigInterface::METHOD_GRANULARITY:
return $this->getMethodsForMethodGranularity();
case RestResourceConfigInterface::RESOURCE_GRANULARITY:
return $this->configuration['methods'];
default:
throw new \InvalidArgumentException('Invalid granularity specified.');
}
}
......@@ -136,12 +139,13 @@ protected function getMethodsForMethodGranularity() {
* {@inheritdoc}
*/
public function getAuthenticationProviders($method) {
if ($this->granularity === RestResourceConfigInterface::METHOD_GRANULARITY) {
return $this->getAuthenticationProvidersForMethodGranularity($method);
}
else {
throw new \InvalidArgumentException("A different granularity then 'method' is not supported yet.");
// @todo Add resource-level granularity support in https://www.drupal.org/node/2721595.
switch ($this->granularity) {
case RestResourceConfigInterface::METHOD_GRANULARITY:
return $this->getAuthenticationProvidersForMethodGranularity($method);
case RestResourceConfigInterface::RESOURCE_GRANULARITY:
return $this->configuration['authentication'];
default:
throw new \InvalidArgumentException('Invalid granularity specified.');
}
}
......@@ -166,12 +170,13 @@ public function getAuthenticationProvidersForMethodGranularity($method) {
* {@inheritdoc}
*/
public function getFormats($method) {
if ($this->granularity === RestResourceConfigInterface::METHOD_GRANULARITY) {
return $this->getFormatsForMethodGranularity($method);
}
else {
throw new \InvalidArgumentException("A different granularity then 'method' is not supported yet.");
// @todo Add resource-level granularity support in https://www.drupal.org/node/2721595.
switch ($this->granularity) {
case RestResourceConfigInterface::METHOD_GRANULARITY:
return $this->getFormatsForMethodGranularity($method);
case RestResourceConfigInterface::RESOURCE_GRANULARITY:
return $this->configuration['formats'];
default:
throw new \InvalidArgumentException('Invalid granularity specified.');
}
}
......
......@@ -15,6 +15,11 @@ interface RestResourceConfigInterface extends ConfigEntityInterface, EntityWithP
*/
const METHOD_GRANULARITY = 'method';
/**
* Granularity value for per-resource configuration.
*/
const RESOURCE_GRANULARITY = 'resource';
/**
* Retrieves the REST resource plugin.
*
......
<?php
namespace Drupal\rest\Tests\Update;
use Drupal\system\Tests\Update\UpdatePathTestBase;
/**
* Tests method-granularity REST config is simplified to resource-granularity.
*
* @see https://www.drupal.org/node/2721595
* @see rest_post_update_resource_granularity()
*
* @group rest
*/
class ResourceGranularityUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['rest', 'serialization'];
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../../rest/tests/fixtures/update/drupal-8.rest-rest_post_update_resource_granularity.php',
];
}
/**
* Tests rest_post_update_simplify_resource_granularity().
*/
public function testMethodGranularityConvertedToResourceGranularity() {
/** @var \Drupal\Core\Entity\EntityStorageInterface $resource_config_storage */
$resource_config_storage = $this->container->get('entity_type.manager')->getStorage('rest_resource_config');
// Make sure we have the expected values before the update.
$resource_config_entities = $resource_config_storage->loadMultiple();
$this->assertIdentical(['entity.comment', 'entity.node', 'entity.user'], array_keys($resource_config_entities));
$this->assertIdentical('method', $resource_config_entities['entity.node']->get('granularity'));
$this->assertIdentical('method', $resource_config_entities['entity.comment']->get('granularity'));
$this->assertIdentical('method', $resource_config_entities['entity.user']->get('granularity'));
// Read the existing 'entity:comment' and 'entity:user' resource
// configuration so we can verify it after the update.
$comment_resource_configuration = $resource_config_entities['entity.comment']->get('configuration');
$user_resource_configuration = $resource_config_entities['entity.user']->get('configuration');
$this->runUpdates();
// Make sure we have the expected values after the update.
$resource_config_entities = $resource_config_storage->loadMultiple();
$this->assertIdentical(['entity.comment', 'entity.node', 'entity.user'], array_keys($resource_config_entities));
// 'entity:node' should be updated.
$this->assertIdentical('resource', $resource_config_entities['entity.node']->get('granularity'));
$this->assertidentical($resource_config_entities['entity.node']->get('configuration'), [
'methods' => ['GET', 'POST', 'PATCH', 'DELETE'],
'formats' => ['hal_json'],
'authentication' => ['basic_auth'],
]);
// 'entity:comment' should be unchanged.
$this->assertIdentical('method', $resource_config_entities['entity.comment']->get('granularity'));
$this->assertIdentical($comment_resource_configuration, $resource_config_entities['entity.comment']->get('configuration'));
// 'entity:user' should be unchanged.
$this->assertIdentical('method', $resource_config_entities['entity.user']->get('granularity'));
$this->assertIdentical($user_resource_configuration, $resource_config_entities['entity.user']->get('configuration'));
}
}
......@@ -9,6 +9,8 @@
* Tests that rest.settings is converted to rest_resource_config entities.
*
* @see https://www.drupal.org/node/2308745
* @see rest_update_8201()
* @see rest_post_update_create_rest_resource_config_entities()
*
* @group rest
*/
......@@ -43,10 +45,6 @@ public function testResourcesConvertedToConfigEntities() {
$resource_config_entities = $resource_config_storage->loadMultiple();
$this->assertIdentical([], array_keys($resource_config_entities));
// Read the existing 'entity:node' resource configuration so we can verify
// it after the update.
$node_configuration = $rest_settings->getRawData()['resources']['entity:node'];
$this->runUpdates();
// Make sure we have the expected values after the update.
......@@ -55,8 +53,12 @@ public function testResourcesConvertedToConfigEntities() {
$resource_config_entities = $resource_config_storage->loadMultiple();
$this->assertIdentical(['entity.node'], array_keys($resource_config_entities));
$node_resource_config_entity = $resource_config_entities['entity.node'];
$this->assertIdentical(RestResourceConfigInterface::METHOD_GRANULARITY, $node_resource_config_entity->get('granularity'));
$this->assertIdentical($node_configuration, $node_resource_config_entity->get('configuration'));
$this->assertIdentical(RestResourceConfigInterface::RESOURCE_GRANULARITY, $node_resource_config_entity->get('granularity'));
$this->assertIdentical([
'methods' => ['GET'],
'formats' => ['json'],
'authentication' => ['basic_auth'],
], $node_resource_config_entity->get('configuration'));
$this->assertIdentical(['module' => ['basic_auth', 'node', 'serialization']], $node_resource_config_entity->getDependencies());
}
......
id: entity.comment
plugin_id: 'entity:comment'
granularity: method
configuration:
GET:
supported_formats:
- hal_json
# This resource has a method-specific format.
# @see \Drupal\rest\Tests\Update\ResourceGranularityUpdateTest
- xml
supported_auth:
- basic_auth
POST:
supported_formats:
- hal_json
supported_auth:
- basic_auth
PATCH:
supported_formats:
- hal_json
supported_auth:
- basic_auth
DELETE:
supported_formats:
- hal_json
supported_auth:
- basic_auth
dependencies:
module:
- node
- basic_auth
- hal
id: entity.node
plugin_id: 'entity:node'
granularity: method
configuration:
GET:
supported_formats:
- hal_json
supported_auth:
- basic_auth
POST:
supported_formats:
- hal_json
supported_auth:
- basic_auth
PATCH:
supported_formats:
- hal_json
supported_auth:
- basic_auth
DELETE:
supported_formats:
- hal_json
supported_auth:
- basic_auth
dependencies:
module:
- node
- basic_auth
- hal
id: entity.user
plugin_id: 'entity:user'
granularity: method
configuration:
GET:
supported_formats:
- hal_json
supported_auth:
- basic_auth
# This resource has a method-specific authentication.
# @see \Drupal\rest\Tests\Update\ResourceGranularityUpdateTest
- oauth
POST:
supported_formats:
- hal_json
supported_auth:
- basic_auth
PATCH:
supported_formats:
- hal_json
supported_auth:
- basic_auth
DELETE:
supported_formats:
- hal_json
supported_auth:
- basic_auth
dependencies:
module:
- node
- basic_auth
- hal
......@@ -21,72 +21,78 @@ class ConfigDependenciesTest extends KernelTestBase {
/**
* @covers ::calculateDependencies
* @covers ::calculateDependenciesForMethodGranularity
*
* @dataProvider providerBasicDependencies
*/
public function testCalculateDependencies() {
public function testCalculateDependencies(array $configuration) {
$config_dependencies = new ConfigDependencies(['hal_json' => 'hal', 'json' => 'serialization'], ['basic_auth' => 'basic_auth']);
$rest_config = RestResourceConfig::create([
'plugin_id' => 'entity:entity_test',
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
'configuration' => [
'GET' => [
'supported_auth' => ['cookie'],
'supported_formats' => ['json'],
],
'POST' => [
'supported_auth' => ['basic_auth'],
'supported_formats' => ['hal_json'],
],
],
]);
$rest_config = RestResourceConfig::create($configuration);
$result = $config_dependencies->calculateDependencies($rest_config);
$this->assertEquals(['module' => [
'serialization', 'basic_auth', 'hal',
'basic_auth', 'hal', 'serialization',
]], $result);
}
/**
* @covers ::onDependencyRemoval
* @covers ::calculateDependenciesForMethodGranularity
* @covers ::onDependencyRemovalForMethodGranularity
* @covers ::onDependencyRemovalForResourceGranularity
*
* @dataProvider providerBasicDependencies
*/
public function testOnDependencyRemovalRemoveUnrelatedDependency() {
public function testOnDependencyRemovalRemoveUnrelatedDependency(array $configuration) {
$config_dependencies = new ConfigDependencies(['hal_json' => 'hal', 'json' => 'serialization'], ['basic_auth' => 'basic_auth']);
$rest_config = RestResourceConfig::create([
'plugin_id' => 'entity:entity_test',
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
'configuration' => [
'GET' => [
'supported_auth' => ['cookie'],
'supported_formats' => ['json'],
],
'POST' => [
'supported_auth' => ['basic_auth'],
'supported_formats' => ['hal_json'],
],
],
]);
$rest_config = RestResourceConfig::create($configuration);
$this->assertFalse($config_dependencies->onDependencyRemoval($rest_config, ['module' => ['node']]));
$this->assertEquals([
'GET' => [
'supported_auth' => ['cookie'],
'supported_formats' => ['json'],
$this->assertEquals($configuration['configuration'], $rest_config->get('configuration'));
}
/**
* @return array
* An array with numerical keys:
* 0. The original REST resource configuration.
*/
public function providerBasicDependencies() {
return [
'method' => [
[
'plugin_id' => 'entity:entity_test',
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
'configuration' => [
'GET' => [
'supported_auth' => ['cookie'],
'supported_formats' => ['json'],
],
'POST' => [
'supported_auth' => ['basic_auth'],
'supported_formats' => ['hal_json'],
],
],
],
],
'POST' => [
'supported_auth' => ['basic_auth'],
'supported_formats' => ['hal_json'],
'resource' => [
[
'plugin_id' => 'entity:entity_test',
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
'configuration' => [
'methods' => ['GET', 'POST'],
'formats' => ['json', 'hal_json'],
'authentication' => ['cookie', 'basic_auth'],
],
],
],
], $rest_config->get('configuration'));
];
}
/**
* @covers ::onDependencyRemoval
* @covers ::calculateDependenciesForMethodGranularity
* @covers ::onDependencyRemovalForMethodGranularity
*/
public function testOnDependencyRemovalRemoveFormat() {
public function testOnDependencyRemovalRemoveFormatForMethodGranularity() {
$config_dependencies = new ConfigDependencies(['hal_json' => 'hal', 'json' => 'serialization'], ['basic_auth' => 'basic_auth']);
$rest_config = RestResourceConfig::create([
......@@ -120,7 +126,7 @@ public function testOnDependencyRemovalRemoveFormat() {
/**
* @covers ::onDependencyRemoval
* @covers ::calculateDependenciesForMethodGranularity
* @covers ::onDependencyRemovalForMethodGranularity