From c7f47000ee205440ebb07217809aa3cc71e0378b Mon Sep 17 00:00:00 2001
From: xjm <xjm@65776.no-reply.drupal.org>
Date: Thu, 21 Feb 2019 08:26:15 -0600
Subject: [PATCH] Issue #3028490 by tim.plunkett, Kristen Pol, xjm, tedbow,
 r.aubin, phenaproxima: Users with "configure any layout" can see entities
 they don't have "view" access to

---
 .../OverridesSectionStorage.php               |  12 +-
 .../src/Functional/LayoutBuilderTest.php      |  32 ++++++
 .../src/Unit/OverridesSectionStorageTest.php  | 104 ++++++++++++++++++
 3 files changed, 144 insertions(+), 4 deletions(-)

diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
index a488f2377c12..0454814bfe50 100644
--- a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -227,13 +227,17 @@ private function extractEntityFromRoute($value, array $defaults) {
    */
   public function buildRoutes(RouteCollection $collection) {
     foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
+      // If the canonical route does not exist, do not provide any Layout
+      // Builder UI routes for this entity type.
+      if (!$collection->get("entity.$entity_type_id.canonical")) {
+        continue;
+      }
+
       $defaults = [];
       $defaults['entity_type_id'] = $entity_type_id;
 
-      $requirements = [];
-      if ($this->hasIntegerId($entity_type)) {
-        $requirements[$entity_type_id] = '\d+';
-      }
+      // Retrieve the requirements from the canonical route.
+      $requirements = $collection->get("entity.$entity_type_id.canonical")->getRequirements();
 
       $options = [];
       // Ensure that upcasting is run in the correct order.
diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
index 77dcb64ac597..b3b1a4d90df0 100644
--- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
+++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
@@ -283,6 +283,38 @@ public function testLayoutBuilderUi() {
     $assert_session->elementNotExists('css', '.field--name-field-my-text');
   }
 
+  /**
+   * Test that layout builder checks entity view access.
+   */
+  public function testAccess() {
+    $assert_session = $this->assertSession();
+
+    $this->drupalLogin($this->drupalCreateUser([
+      'configure any layout',
+      'administer node display',
+    ]));
+
+    $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
+    // Allow overrides for the layout.
+    $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[enabled]' => TRUE], 'Save');
+    $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save');
+
+    $this->drupalLogin($this->drupalCreateUser(['configure any layout']));
+    $this->drupalGet('node/1');
+    $assert_session->pageTextContains('The first node body');
+    $assert_session->pageTextNotContains('Powered by Drupal');
+    $node = Node::load(1);
+    $node->setUnpublished();
+    $node->save();
+    $this->drupalGet('node/1');
+    $assert_session->pageTextNotContains('The first node body');
+    $assert_session->pageTextContains('Access denied');
+
+    $this->drupalGet('node/1/layout');
+    $assert_session->pageTextNotContains('The first node body');
+    $assert_session->pageTextContains('Access denied');
+  }
+
   /**
    * Tests that a non-default view mode works as expected.
    */
diff --git a/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php
index 93fd799323e5..af2ae0d12ea3 100644
--- a/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php
+++ b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php
@@ -294,6 +294,23 @@ public function testBuildRoutes() {
     $entity_types['no_canonical_link'] = $no_canonical_link->reveal();
     $this->entityFieldManager->getFieldStorageDefinitions('no_canonical_link')->shouldNotBeCalled();
 
+    $canonical_link_no_route = $this->prophesize(EntityTypeInterface::class);
+    $canonical_link_no_route->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE);
+    $canonical_link_no_route->hasViewBuilderClass()->willReturn(TRUE);
+    $canonical_link_no_route->hasLinkTemplate('canonical')->willReturn(TRUE);
+    $canonical_link_no_route->getLinkTemplate('canonical')->willReturn('/entity/{entity}');
+    $canonical_link_no_route->hasHandlerClass('form', 'layout_builder')->willReturn(TRUE);
+    $entity_types['canonical_link_no_route'] = $canonical_link_no_route->reveal();
+    $this->entityFieldManager->getFieldStorageDefinitions('canonical_link_no_route')->shouldNotBeCalled();
+
+    $from_canonical = $this->prophesize(EntityTypeInterface::class);
+    $from_canonical->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE);
+    $from_canonical->hasViewBuilderClass()->willReturn(TRUE);
+    $from_canonical->hasLinkTemplate('canonical')->willReturn(TRUE);
+    $from_canonical->getLinkTemplate('canonical')->willReturn('/entity/{entity}');
+    $from_canonical->hasHandlerClass('form', 'layout_builder')->willReturn(TRUE);
+    $entity_types['from_canonical'] = $from_canonical->reveal();
+
     $with_string_id = $this->prophesize(EntityTypeInterface::class);
     $with_string_id->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE);
     $with_string_id->hasViewBuilderClass()->willReturn(TRUE);
@@ -323,6 +340,87 @@ public function testBuildRoutes() {
     $this->entityTypeManager->getDefinitions()->willReturn($entity_types);
 
     $expected = [
+      'entity.from_canonical.canonical' => new Route(
+        '/entity/{entity}',
+        [],
+        [
+          'custom requirement' => 'from_canonical_route',
+        ]
+      ),
+      'entity.with_string_id.canonical' => new Route(
+        '/entity/{entity}'
+      ),
+      'entity.with_integer_id.canonical' => new Route(
+        '/entity/{entity}',
+        [],
+        [
+          'with_integer_id' => '\d+',
+        ]
+      ),
+      'layout_builder.overrides.from_canonical.view' => new Route(
+        '/entity/{entity}/layout',
+        [
+          'entity_type_id' => 'from_canonical',
+          'section_storage_type' => 'overrides',
+          'section_storage' => '',
+          '_entity_form' => 'from_canonical.layout_builder',
+          '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title',
+        ],
+        [
+          '_has_layout_section' => 'true',
+          '_layout_builder_access' => 'view',
+          'custom requirement' => 'from_canonical_route',
+        ],
+        [
+          'parameters' => [
+            'section_storage' => ['layout_builder_tempstore' => TRUE],
+            'from_canonical' => ['type' => 'entity:from_canonical'],
+          ],
+          '_layout_builder' => TRUE,
+        ]
+      ),
+      'layout_builder.overrides.from_canonical.discard_changes' => new Route(
+        '/entity/{entity}/layout/discard-changes',
+        [
+          'entity_type_id' => 'from_canonical',
+          'section_storage_type' => 'overrides',
+          'section_storage' => '',
+          '_form' => '\Drupal\layout_builder\Form\DiscardLayoutChangesForm',
+        ],
+        [
+          '_has_layout_section' => 'true',
+          '_layout_builder_access' => 'view',
+          'custom requirement' => 'from_canonical_route',
+        ],
+        [
+          'parameters' => [
+            'section_storage' => ['layout_builder_tempstore' => TRUE],
+            'from_canonical' => ['type' => 'entity:from_canonical'],
+          ],
+          '_layout_builder' => TRUE,
+        ]
+      ),
+      'layout_builder.overrides.from_canonical.revert' => new Route(
+        '/entity/{entity}/layout/revert',
+        [
+          'entity_type_id' => 'from_canonical',
+          'section_storage_type' => 'overrides',
+          'section_storage' => '',
+          '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm',
+        ],
+        [
+          '_has_layout_section' => 'true',
+          '_layout_builder_access' => 'view',
+          'custom requirement' => 'from_canonical_route',
+        ],
+        [
+          'parameters' => [
+            'section_storage' => ['layout_builder_tempstore' => TRUE],
+            'from_canonical' => ['type' => 'entity:from_canonical'],
+          ],
+          '_layout_builder' => TRUE,
+        ]
+      ),
       'layout_builder.overrides.with_string_id.view' => new Route(
         '/entity/{entity}/layout',
         [
@@ -451,6 +549,12 @@ public function testBuildRoutes() {
     ];
 
     $collection = new RouteCollection();
+    // Entity types that declare a link template for canonical must have a
+    // canonical route present in the route colletion.
+    $collection->add('entity.from_canonical.canonical', $expected['entity.from_canonical.canonical']);
+    $collection->add('entity.with_string_id.canonical', $expected['entity.with_string_id.canonical']);
+    $collection->add('entity.with_integer_id.canonical', $expected['entity.with_integer_id.canonical']);
+
     $this->plugin->buildRoutes($collection);
     $this->assertEquals($expected, $collection->all());
     $this->assertSame(array_keys($expected), array_keys($collection->all()));
-- 
GitLab