From 4b2656695a3f4dbea35962fccb540fbd3fb91d2e Mon Sep 17 00:00:00 2001 From: Alex Pott <alex.a.pott@googlemail.com> Date: Fri, 8 Apr 2016 13:47:07 +0100 Subject: [PATCH] Issue #2696669 by mbovan, Berdir, sanja_m: Incomplete/broken access checking in EntityController::addPage() --- core/core.services.yml | 5 + .../Entity/Controller/EntityController.php | 2 +- .../Entity/EntityCreateAnyAccessCheck.php | 109 ++++++++++++++++++ .../Routing/DefaultHtmlRouteProvider.php | 12 +- .../src/Tests/Entity/EntityAddUITest.php | 75 +++++++++--- .../entity_test/entity_test.permissions.yml | 5 + .../src/Entity/EntityTestBundle.php | 2 +- .../src/Entity/EntityTestWithBundle.php | 2 +- .../src/EntityTestAccessControlHandler.php | 7 +- .../entity_test/src/EntityTestPermissions.php | 57 +++++++++ .../Routing/DefaultHtmlRouteProviderTest.php | 12 +- 11 files changed, 264 insertions(+), 24 deletions(-) create mode 100644 core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php create mode 100644 core/modules/system/tests/modules/entity_test/src/EntityTestPermissions.php diff --git a/core/core.services.yml b/core/core.services.yml index 5e63508f6c4b..6a31a44f6177 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1069,6 +1069,11 @@ services: arguments: ['@entity.manager'] tags: - { name: access_check, applies_to: _entity_create_access } + access_check.entity_create_any: + class: Drupal\Core\Entity\EntityCreateAnyAccessCheck + arguments: ['@entity_type.manager', '@entity_type.bundle.info'] + tags: + - { name: access_check, applies_to: _entity_create_any_access } access_check.theme: class: Drupal\Core\Theme\ThemeAccessCheck arguments: ['@theme_handler'] diff --git a/core/lib/Drupal/Core/Entity/Controller/EntityController.php b/core/lib/Drupal/Core/Entity/Controller/EntityController.php index 5ae33ef6269e..0392fcf23e68 100644 --- a/core/lib/Drupal/Core/Entity/Controller/EntityController.php +++ b/core/lib/Drupal/Core/Entity/Controller/EntityController.php @@ -140,7 +140,7 @@ public function addPage($entity_type_id) { '@add_link' => Link::createFromRoute($link_text, $link_route_name)->toString(), ]); // Filter out the bundles the user doesn't have access to. - $access_control_handler = $this->entityTypeManager->getAccessControlHandler($bundle_entity_type_id); + $access_control_handler = $this->entityTypeManager->getAccessControlHandler($entity_type_id); foreach ($bundles as $bundle_name => $bundle_info) { $access = $access_control_handler->createAccess($bundle_name, NULL, [], TRUE); if (!$access->isAllowed()) { diff --git a/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php new file mode 100644 index 000000000000..7ca49c0fa08c --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php @@ -0,0 +1,109 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Entity\EntityCreateAnyAccessCheck. + */ + +namespace Drupal\Core\Entity; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Routing\Access\AccessInterface; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Session\AccountInterface; +use Symfony\Component\Routing\Route; + +/** + * Defines an access checker for creating an entity of any bundle. + */ +class EntityCreateAnyAccessCheck implements AccessInterface { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The entity type bundle info. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface + */ + protected $entityTypeBundleInfo; + + /** + * The key used by the routing requirement. + * + * @var string + */ + protected $requirementsKey = '_entity_create_any_access'; + + /** + * Constructs a EntityCreateAnyAccessCheck object. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * The entity type bundle info. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info) { + $this->entityTypeManager = $entity_type_manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + } + + /** + * Checks access to create an entity of any bundle for the given route. + * + * @param \Symfony\Component\Routing\Route $route + * The route to check against. + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The parameterized route. + * @param \Drupal\Core\Session\AccountInterface $account + * The currently logged in account. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + */ + public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) { + $entity_type_id = $route->getRequirement($this->requirementsKey); + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + $access_control_handler = $this->entityTypeManager->getAccessControlHandler($entity_type_id); + + // In case there is no "bundle" entity key, check create access with no + // bundle specified. + if (!$entity_type->hasKey('bundle')) { + return $access_control_handler->createAccess(NULL, $account, [], TRUE); + } + + $access = AccessResult::neutral(); + $bundles = array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id)); + + // Include list cache tag as access might change if more bundles are added. + if ($entity_type->getBundleEntityType()) { + $access->addCacheTags($this->entityTypeManager->getDefinition($entity_type->getBundleEntityType())->getListCacheTags()); + + // Check if the user is allowed to create new bundles. If so, allow + // access, so the add page can show a link to create one. + // @see \Drupal\Core\Entity\Controller\EntityController::addPage() + $bundle_access_control_handler = $this->entityTypeManager->getAccessControlHandler($entity_type->getBundleEntityType()); + $access = $access->orIf($bundle_access_control_handler->createAccess(NULL, $account, [], TRUE)); + if ($access->isAllowed()) { + return $access; + } + } + + // Check whether an entity of any bundle may be created. + foreach ($bundles as $bundle) { + $access = $access->orIf($access_control_handler->createAccess($bundle, $account, [], TRUE)); + // In case there is a least one bundle user can create entities for, + // access is allowed. + if ($access->isAllowed()) { + break; + } + } + + return $access; + } + +} diff --git a/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php b/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php index 26258bd638da..6b9c29fce0ba 100644 --- a/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php +++ b/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php @@ -120,7 +120,7 @@ protected function getAddPageRoute(EntityTypeInterface $entity_type) { $route->setDefault('_controller', EntityController::class . '::addPage'); $route->setDefault('_title_callback', EntityController::class . '::addTitle'); $route->setDefault('entity_type_id', $entity_type->id()); - $route->setRequirement('_entity_create_access', $entity_type->id()); + $route->setRequirement('_entity_create_any_access', $entity_type->id()); return $route; } @@ -151,7 +151,11 @@ protected function getAddFormRoute(EntityTypeInterface $entity_type) { // If the entity has bundles, we can provide a bundle-specific title // and access requirements. - if ($bundle_key = $entity_type->getKey('bundle')) { + $expected_parameter = $entity_type->getBundleEntityType() ?: $entity_type->getKey('bundle'); + // @todo: We have to check if a route contains a bundle in its path as + // test entities have inconsistent usage of "add-form" link templates. + // Fix it in https://www.drupal.org/node/2699959. + if (($bundle_key = $entity_type->getKey('bundle')) && strpos($route->getPath(), '{' . $expected_parameter . '}') !== FALSE) { $route->setDefault('_title_callback', EntityController::class . '::addBundleTitle'); // If the bundles are entities themselves, we can add parameter // information to the route options. @@ -162,7 +166,7 @@ protected function getAddFormRoute(EntityTypeInterface $entity_type) { // The title callback uses the value of the bundle parameter to // fetch the respective bundle at runtime. ->setDefault('bundle_parameter', $bundle_entity_type_id) - ->setRequirement('_entity_create_access', "{$entity_type_id}:{$bundle_entity_type_id}"); + ->setRequirement('_entity_create_access', $entity_type_id . ':{' . $bundle_entity_type_id . '}'); // Entity types with serial IDs can specify this in their route // requirements, improving the matching process. @@ -185,7 +189,7 @@ protected function getAddFormRoute(EntityTypeInterface $entity_type) { // route parameter name directly. $route ->setDefault('bundle_parameter', $bundle_key) - ->setRequirement('_entity_create_access', "{$entity_type_id}:{$bundle_key}"); + ->setRequirement('_entity_create_access', $entity_type_id . ':{' . $bundle_key . '}'); } } else { diff --git a/core/modules/system/src/Tests/Entity/EntityAddUITest.php b/core/modules/system/src/Tests/Entity/EntityAddUITest.php index 0df247a47eb7..c5fe9691b9ac 100644 --- a/core/modules/system/src/Tests/Entity/EntityAddUITest.php +++ b/core/modules/system/src/Tests/Entity/EntityAddUITest.php @@ -25,24 +25,28 @@ class EntityAddUITest extends WebTestBase { public static $modules = ['entity_test']; /** - * {@inheritdoc} + * Tests the add page for an entity type using bundle entities. */ - protected function setUp() { - parent::setUp(); + public function testAddPageWithBundleEntities() { + $admin_user = $this->drupalCreateUser([ + 'administer entity_test_with_bundle content', + ]); + $this->drupalLogin($admin_user); - $web_user = $this->drupalCreateUser([ - "administer entity_test_with_bundle content", - "administer entity_test content", + // Users without create access for bundles do not have access to the add + // page if there are no bundles. + $this->drupalGet('/entity_test_with_bundle/add'); + $this->assertResponse(403); + + $bundle_admin_user = $this->drupalCreateUser([ + 'administer entity_test_with_bundle content', + 'administer entity_test_bundle content', ]); - $this->drupalLogin($web_user); - } + $this->drupalLogin($bundle_admin_user); - /** - * Tests the add page for an entity type using bundle entities. - */ - public function testAddPageWithBundleEntities() { + // No bundles exist, the add bundle message should be present as the user + // has the necessary permissions. $this->drupalGet('/entity_test_with_bundle/add'); - // No bundles exist, the add bundle message should be present. $this->assertText('There is no test entity bundle yet.'); $this->assertLink('Add a new test entity bundle.'); @@ -74,12 +78,57 @@ public function testAddPageWithBundleEntities() { $this->drupalPostForm(NULL, ['name[0][value]' => 'test name'], t('Save')); $entity = EntityTestWithBundle::load(1); $this->assertEqual('test name', $entity->label()); + + // Create a new user that only has bundle specific permissions. + $user = $this->drupalCreateUser([ + 'create test entity_test_with_bundle entities', + 'create test2 entity_test_with_bundle entities', + ]); + $this->drupalLogin($user); + + // Create a bundle that the user does not have create permissions for. + EntityTestBundle::create([ + 'id' => 'test3', + 'label' => 'Test3 label', + 'description' => 'My test3 description', + ])->save(); + $this->drupalGet('/entity_test_with_bundle/add'); + $this->assertLink('Test label'); + $this->assertLink('Test2 label'); + $this->assertNoLink('Test3 label'); + $this->clickLink(t('Test label')); + $this->assertResponse(200); + + // Without any permissions, access must be denied. + $this->drupalLogout(); + $this->drupalGet('/entity_test_with_bundle/add'); + $this->assertResponse(403); + + // Create a new user that has bundle create permissions. + $user = $this->drupalCreateUser([ + 'administer entity_test_bundle content', + ]); + $this->drupalLogin($user); + + // User has access to the add page but no bundles are shown because the user + // does not have bundle specific permissions. The add bundle message is + // present as the user has bundle create permissions. + $this->drupalGet('/entity_test_with_bundle/add'); + $this->assertNoLink('Test label'); + $this->assertNoLink('Test2 label'); + $this->assertNoLink('Test3 label'); + $this->assertLink('Add a new test entity bundle.'); } /** * Tests the add page for an entity type not using bundle entities. */ public function testAddPageWithoutBundleEntities() { + $admin_user = $this->drupalCreateUser([ + 'administer entity_test content', + ]); + $this->drupalLogin($admin_user); + entity_test_create_bundle('test', 'Test label', 'entity_test_mul'); // Delete the default bundle, so that we can rely on our own. entity_test_delete_bundle('entity_test_mul', 'entity_test_mul'); diff --git a/core/modules/system/tests/modules/entity_test/entity_test.permissions.yml b/core/modules/system/tests/modules/entity_test/entity_test.permissions.yml index eb0d974acd20..ca04459ad960 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.permissions.yml +++ b/core/modules/system/tests/modules/entity_test/entity_test.permissions.yml @@ -10,3 +10,8 @@ view test entity field: administer entity_test_with_bundle content: title: 'administer entity_test_with_bundle content' description: 'administer entity_test_with_bundle content' +administer entity_test_bundle content: + title: 'administer entity_test_bundle content' + +permission_callbacks: + - \Drupal\entity_test\EntityTestPermissions::entityTestBundlePermissions diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBundle.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBundle.php index f9abd9b96342..5d7557ddf424 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBundle.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBundle.php @@ -25,7 +25,7 @@ * "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider", * }, * }, - * admin_permission = "administer entity_test_with_bundle content", + * admin_permission = "administer entity_test_bundle content", * config_prefix = "entity_test_bundle", * bundle_of = "entity_test_with_bundle", * entity_keys = { diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php index c06c8e37f6f6..c78c4f88a1fa 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php @@ -20,7 +20,7 @@ * handlers = { * "list_builder" = "Drupal\entity_test\EntityTestListBuilder", * "view_builder" = "Drupal\entity_test\EntityTestViewBuilder", - * "access" = "\Drupal\Core\Entity\EntityAccessControlHandler", + * "access" = "Drupal\entity_test\EntityTestAccessControlHandler", * "form" = { * "default" = "\Drupal\Core\Entity\ContentEntityForm", * "delete" = "\Drupal\Core\Entity\EntityDeleteForm" diff --git a/core/modules/system/tests/modules/entity_test/src/EntityTestAccessControlHandler.php b/core/modules/system/tests/modules/entity_test/src/EntityTestAccessControlHandler.php index 79880604504d..cd6dd3138f58 100644 --- a/core/modules/system/tests/modules/entity_test/src/EntityTestAccessControlHandler.php +++ b/core/modules/system/tests/modules/entity_test/src/EntityTestAccessControlHandler.php @@ -22,6 +22,7 @@ * @see \Drupal\entity_test\Entity\EntityTestMul * @see \Drupal\entity_test\Entity\EntityTestMulRev * @see \Drupal\entity_test\Entity\EntityTestRev + * @see \Drupal\entity_test\Entity\EntityTestWithBundle * @see \Drupal\entity_test\Entity\EntityTestStringId */ class EntityTestAccessControlHandler extends EntityAccessControlHandler { @@ -68,7 +69,11 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter * {@inheritdoc} */ protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { - return AccessResult::allowedIfHasPermission($account, 'administer entity_test content'); + return AccessResult::allowedIfHasPermissions($account, [ + 'administer entity_test content', + 'administer entity_test_with_bundle content', + 'create ' . $entity_bundle . ' entity_test_with_bundle entities', + ], 'OR'); } } diff --git a/core/modules/system/tests/modules/entity_test/src/EntityTestPermissions.php b/core/modules/system/tests/modules/entity_test/src/EntityTestPermissions.php new file mode 100644 index 000000000000..ab9f1faee374 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test/src/EntityTestPermissions.php @@ -0,0 +1,57 @@ +<?php + +/** + * @file + * Contains \Drupal\entity_test\EntityTestPermissions. + */ + +namespace Drupal\entity_test; + +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\entity_test\Entity\EntityTestBundle; + +/** + * Provides dynamic permissions for entity test. + */ +class EntityTestPermissions { + + use StringTranslationTrait; + + /** + * Returns an array of entity_test_bundle permissions. + * + * @return array + * An array of entity_test_bundle permissions. + * @see \Drupal\user\PermissionHandlerInterface::getPermissions() + */ + public function entityTestBundlePermissions() { + $perms = []; + // Generate permissions for all EntityTestBundle bundles. + foreach (EntityTestBundle::loadMultiple() as $type) { + $perms += $this->buildPermissions($type); + } + + return $perms; + } + + /** + * Returns a list of entity test permissions for a given entity test bundle. + * + * @param EntityTestBundle $type + * The entity test bundle. + * + * @return array + * An associative array of permission names and descriptions. + */ + protected function buildPermissions(EntityTestBundle $type) { + $type_id = $type->id(); + $type_params = ['%type_name' => $type->label()]; + + return [ + "create $type_id entity_test_with_bundle entities" => [ + 'title' => $this->t('%type_name: Create new entity', $type_params), + ], + ]; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php b/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php index 54393d44504d..71966527602c 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php @@ -90,7 +90,7 @@ public function providerTestGetAddPageRoute() { '_title_callback' => 'Drupal\Core\Entity\Controller\EntityController::addTitle', 'entity_type_id' => 'the_entity_type_id', ]); - $route->setRequirement('_entity_create_access', 'the_entity_type_id'); + $route->setRequirement('_entity_create_any_access', 'the_entity_type_id'); $data['add_page'] = [clone $route, $entity_type3->reveal()]; return $data; @@ -119,9 +119,11 @@ public function providerTestGetAddFormRoute() { $entity_type1 = $this->getEntityType(); $entity_type1->hasLinkTemplate('add-form')->willReturn(FALSE); + $data['no_add_form_link_template'] = [NULL, $entity_type1->reveal()]; $entity_type2 = $this->getEntityType(); + $entity_type2->getBundleEntityType()->willReturn(NULL); $entity_type2->hasLinkTemplate('add-form')->willReturn(TRUE); $entity_type2->id()->willReturn('the_entity_type_id'); $entity_type2->getLinkTemplate('add-form')->willReturn('/the/add/form/link/template'); @@ -145,19 +147,23 @@ public function providerTestGetAddFormRoute() { $entity_type4 = $this->getEntityType($entity_type3); $entity_type4->getKey('bundle')->willReturn('the_bundle_key'); $entity_type4->getBundleEntityType()->willReturn(NULL); + $entity_type4->getLinkTemplate('add-form')->willReturn('/the/add/form/link/template/{the_bundle_key}'); + $route->setPath('/the/add/form/link/template/{the_bundle_key}'); $route ->setDefault('_title_callback', 'Drupal\Core\Entity\Controller\EntityController::addBundleTitle') ->setDefault('bundle_parameter', 'the_bundle_key') - ->setRequirement('_entity_create_access', 'the_entity_type_id:the_bundle_key'); + ->setRequirement('_entity_create_access', 'the_entity_type_id:{the_bundle_key}'); $data['add_form_bundle_static'] = [clone $route, $entity_type4->reveal()]; $entity_type5 = $this->getEntityType($entity_type4); $entity_type5->getBundleEntityType()->willReturn('the_bundle_entity_type_id'); + $entity_type5->getLinkTemplate('add-form')->willReturn('/the/add/form/link/template/{the_bundle_entity_type_id}'); $bundle_entity_type = $this->getEntityType(); $bundle_entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE); + $route->setPath('/the/add/form/link/template/{the_bundle_entity_type_id}'); $route ->setDefault('bundle_parameter', 'the_bundle_entity_type_id') - ->setRequirement('_entity_create_access', 'the_entity_type_id:the_bundle_entity_type_id') + ->setRequirement('_entity_create_access', 'the_entity_type_id:{the_bundle_entity_type_id}') ->setOption('parameters', ['the_bundle_entity_type_id' => [ 'type' => 'entity:the_bundle_entity_type_id', ]]); -- GitLab