Issue #3028301 by tedbow, tim.plunkett, Wim Leers, xjm: Do not expose to Layout Builder's sections either in defaults or overrides to REST and other external APIs

8.7.x
xjm 2019-02-08 16:20:08 -06:00
parent 902611a1dc
commit d8859389a3
6 changed files with 421 additions and 0 deletions

View File

@ -2,8 +2,10 @@
namespace Drupal\layout_builder\Field;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionListInterface;
use Drupal\layout_builder\SectionStorage\SectionStorageTrait;
@ -74,4 +76,12 @@ class LayoutSectionItemList extends FieldItemList implements SectionListInterfac
return $convert($this) === $convert($list_to_compare);
}
/**
* {@inheritdoc}
*/
public function defaultAccess($operation = 'view', AccountInterface $account = NULL) {
// @todo Allow access in https://www.drupal.org/node/2942975.
return AccessResult::forbidden();
}
}

View File

@ -5,6 +5,8 @@ namespace Drupal\layout_builder;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Drupal\layout_builder\EventSubscriber\SetInlineBlockDependency;
use Drupal\layout_builder\Normalizer\LayoutEntityDisplayNormalizer;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
@ -35,6 +37,14 @@ class LayoutBuilderServiceProvider implements ServiceProviderInterface {
$definition->addTag('event_subscriber');
$container->setDefinition('layout_builder.get_block_dependency_subscriber', $definition);
}
if (isset($modules['serialization'])) {
$definition = (new ChildDefinition('serializer.normalizer.config_entity'))
->setClass(LayoutEntityDisplayNormalizer::class)
// Ensure that this normalizer takes precedence for Layout Builder data
// over the generic serializer.normalizer.config_entity.
->addTag('normalizer', ['priority' => 5]);
$container->setDefinition('layout_builder.normalizer.layout_entity_display', $definition);
}
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Drupal\layout_builder\Normalizer;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\serialization\Normalizer\ConfigEntityNormalizer;
/**
* Normalizes/denormalizes LayoutEntityDisplay objects into an array structure.
*
* @internal
* All tagged services are internal.
*/
class LayoutEntityDisplayNormalizer extends ConfigEntityNormalizer {
/**
* {@inheritdoc}
*/
protected $supportedInterfaceOrClass = LayoutEntityDisplayInterface::class;
/**
* {@inheritdoc}
*/
protected static function getDataWithoutInternals(array $data) {
$data = parent::getDataWithoutInternals($data);
// Do not expose the actual layout sections in normalization.
// @todo Determine what to expose here in
// https://www.drupal.org/node/2942975.
unset($data['third_party_settings']['layout_builder']['sections']);
return $data;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Drupal\Tests\layout_builder\Functional\Rest;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Url;
/**
* Tests that default layout sections are not exposed via the REST API.
*
* @group layout_builder
* @group rest
*/
class EntityDisplaySectionsTest extends LayoutRestTestBase {
/**
* {@inheritdoc}
*/
protected static $resourceConfigId = 'entity.entity_view_display';
/**
* Tests the normalization does not contain layout sections.
*/
public function testLayoutEntityDisplay() {
$display_id = 'node.bundle_with_section_field.default';
$display = EntityViewDisplay::load($display_id);
// Assert the display has 1 section.
$this->assertCount(1, $display->getThirdPartySetting('layout_builder', 'sections'));
$response = $this->request(
'GET',
Url::fromRoute(
'rest.entity.entity_view_display.GET',
['entity_view_display' => 'node.bundle_with_section_field.default'])
);
$this->assertResourceResponse(
200,
FALSE,
$response,
[
'config:core.entity_view_display.node.bundle_with_section_field.default',
'config:rest.resource.entity.entity_view_display',
'config:rest.settings',
'http_response',
],
[
'user.permissions',
],
FALSE,
'MISS'
);
$response_data = $this->getDecodedContents($response);
$this->assertSame($display_id, $response_data['id']);
// Ensure the sections are not present in the serialized data, but other
// Layout Builder data is.
$this->assertArrayHasKey('layout_builder', $response_data['third_party_settings']);
$this->assertArrayNotHasKey('sections', $response_data['third_party_settings']['layout_builder']);
$this->assertEquals(['enabled' => TRUE, 'allow_custom' => TRUE], $response_data['third_party_settings']['layout_builder']);
}
}

View File

@ -0,0 +1,154 @@
<?php
namespace Drupal\Tests\layout_builder\Functional\Rest;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Url;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\ResourceTestBase;
use GuzzleHttp\RequestOptions;
use Psr\Http\Message\ResponseInterface;
/**
* Base class for Layout Builder REST tests.
*/
abstract class LayoutRestTestBase extends ResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'node',
'layout_builder',
'serialization',
'basic_auth',
];
/**
* A test node.
*
* @var \Drupal\node\NodeInterface
*/
protected $node;
/**
* The node storage.
*
* @var \Drupal\node\NodeStorageInterface
*/
protected $nodeStorage;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$assert_session = $this->assertSession();
// @todo The Layout Builder UI relies on local tasks; fix in
// https://www.drupal.org/project/drupal/issues/2917777.
$this->drupalPlaceBlock('local_tasks_block');
$this->createContentType(['type' => 'bundle_with_section_field']);
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer display modes',
'bypass node access',
'create bundle_with_section_field content',
'edit any bundle_with_section_field content',
]));
$page = $this->getSession()->getPage();
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field/display';
// Enable Layout Builder for the default view modes, and overrides.
$this->drupalGet("$field_ui_prefix/default");
$page->checkField('layout[enabled]');
$page->pressButton('Save');
$page->checkField('layout[allow_custom]');
$page->pressButton('Save');
// Create a node.
$this->node = $this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'A node at rest will stay at rest.',
]);
$this->drupalGet('node/' . $this->node->id() . '/layout');
$page->clickLink('Layout');
$page->clickLink('Add Block');
$page->clickLink('Powered by Drupal');
$page->fillField('settings[label]', 'This is an override');
$page->checkField('settings[label_display]');
$page->pressButton('Add Block');
$page->clickLink('Save Layout');
$assert_session->pageTextContains('This is an override');
$this->nodeStorage = $this->container->get('entity_type.manager')->getStorage('node');
$this->node = $this->nodeStorage->load($this->node->id());
$this->drupalLogout();
$this->setUpAuthorization('ALL');
$this->provisionResource([static::$format], ['basic_auth']);
}
/**
* {@inheritdoc}
*/
protected function request($method, Url $url, array $request_options = []) {
$request_options[RequestOptions::HEADERS] = [
'Content-Type' => static::$mimeType,
];
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions($method));
$request_options[RequestOptions::QUERY] = ['_format' => static::$format];
return parent::request($method, $url, $request_options);
}
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$permissions = array_keys($this->container->get('user.permissions')->getPermissions());
// Give the test user all permissions on the site. There should be no
// permission that gives the user access to layout sections over REST.
$this->account = $this->drupalCreateUser($permissions);
}
/**
* {@inheritdoc}
*/
protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {}
/**
* {@inheritdoc}
*/
protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) {}
/**
* {@inheritdoc}
*/
protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options) {}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessCacheability() {}
/**
* Gets the decoded contents.
*
* @param \Psr\Http\Message\ResponseInterface $response
* The response.
*
* @return array
* The decoded contents.
*/
protected function getDecodedContents(ResponseInterface $response) {
return $this->serializer->decode((string) $response->getBody(), static::$format);
}
}

View File

@ -0,0 +1,153 @@
<?php
namespace Drupal\Tests\layout_builder\Functional\Rest;
use Drupal\Core\Url;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\node\Entity\Node;
use GuzzleHttp\RequestOptions;
/**
* Tests that override layout sections are not exposed via the REST API.
*
* @group layout_builder
* @group rest
*/
class OverrideSectionsTest extends LayoutRestTestBase {
/**
* {@inheritdoc}
*/
protected static $resourceConfigId = 'entity.node';
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// @todo Figure why field definitions have to cleared in
// https://www.drupal.org/project/drupal/issues/2985882.
$this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
}
/**
* Tests that the layout override field is not normalized.
*/
public function testOverrideField() {
$this->assertCount(1, $this->node->get(OverridesSectionStorage::FIELD_NAME));
// Make a GET request and ensure override field is not included.
$response = $this->request(
'GET',
Url::fromRoute('rest.entity.node.GET', ['node' => $this->node->id()])
);
$this->assertResourceResponse(
200,
FALSE,
$response,
[
'config:filter.format.plain_text',
'config:rest.resource.entity.node',
'config:rest.settings',
'http_response',
'node:1',
],
[
'languages:language_interface',
'theme',
'url.site',
'user.permissions',
],
FALSE,
'MISS'
);
$get_data = $this->getDecodedContents($response);
$this->assertSame('A node at rest will stay at rest.', $get_data['title'][0]['value']);
$this->assertArrayNotHasKey('layout_builder__layout', $get_data);
// Make a POST request without the override field.
$new_node = [
'type' => [
[
'target_id' => 'bundle_with_section_field',
],
],
'title' => [
[
'value' => 'On with the rest of the test.',
],
],
];
$response = $this->request(
'POST',
Url::fromRoute(
'rest.entity.node.POST'),
[
RequestOptions::BODY => $this->serializer->encode($new_node, static::$format),
]
);
$this->assertResourceResponse(201, FALSE, $response);
$posted_node = $this->nodeStorage->load(2);
$this->assertEquals('On with the rest of the test.', $posted_node->getTitle());
// Make a POST request with override field.
$new_node['layout_builder__layout'] = [];
$post_contents = $this->serializer->encode($new_node, static::$format);
$response = $this->request(
'POST',
Url::fromRoute(
'rest.entity.node.POST'),
[
RequestOptions::BODY => $post_contents,
]
);
$this->assertResourceErrorResponse(403, 'Access denied on creating field \'layout_builder__layout\'.', $response);
// Make a PATCH request without the override field.
$patch_data = [
'title' => [
[
'value' => 'New and improved title',
],
],
'type' => [
[
'target_id' => 'bundle_with_section_field',
],
],
];
$response = $this->request(
'PATCH',
Url::fromRoute(
'rest.entity.node.PATCH',
['node' => 1]
),
[
RequestOptions::BODY => $this->serializer->encode($patch_data, static::$format),
]
);
$this->assertResourceResponse(200, FALSE, $response);
$this->nodeStorage->resetCache([1]);
$this->node = $this->nodeStorage->load(1);
$this->assertEquals('New and improved title', $this->node->getTitle());
// Make a PATCH request with the override field.
$patch_data['title'][0]['value'] = 'This title will not save.';
$patch_data['layout_builder__layout'] = [];
$response = $this->request(
'PATCH',
Url::fromRoute(
'rest.entity.node.PATCH',
['node' => 1]
),
[
RequestOptions::BODY => $this->serializer->encode($patch_data, static::$format),
]
);
$this->assertResourceErrorResponse(403, 'Access denied on updating field \'layout_builder__layout\'.', $response);
// Ensure the title has not changed.
$this->assertEquals('New and improved title', Node::load(1)->getTitle());
}
}