Problem/Motivation

While working on #2691337: Merge crm_core_contacts_ui into crm_core_contacts we discovered the following:
- addPage() checks createAccess() for bundles of the wrong entity type, and the tests aren't complete enough to catch it.
- there is a missing access check to ensure the user can create an entity of at least one bundle, before showing addPage()
- there is an $entity_type_id marker set in route requirements in \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider::getAddFormRoute() which as a result prevents us to access dynamically created "add-form" route.

Proposed resolution

- Replace bundle entity type with correct entity type (entity type of an entity that's being created) in EntityController::addPage()
- Implement EntityCreateAnyAccessCheck to ensure a user can create an entity of at least one bundle.
- Remove placeholders for $entity_type_id in _entity_create_access requirement in \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider::getAddFormRoute()
- Provide tests

Comments

mbovan created an issue. See original summary.

mbovan’s picture

Added related issue where getAddPageRoute() was introduced.

mbovan’s picture

Status: Active » Needs review
StatusFileSize
new3.9 KB

Here is the initial patch, no tests yet. Some thoughts?

bojanz’s picture

Thanks for getting the ball rolling on this.

You should inject the entityTypeManager, not the entityManager (it's deprecated).

berdir’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
@@ -0,0 +1,75 @@
+   */
+  public function __construct(EntityManagerInterface $entity_manager) {
+    $this->entityManager = $entity_manager;
+  }

use entity type manager.

Don't hardcode that there is a bundle entity type. Instead, get the bundles from the EntityBundleInfo service. Also, check if there is a bundle entity key, if not, call createAccess() once, without a bundle.

berdir’s picture

Maybe we can extend the tests from the issue that added add-page, and e.g. extend the test entity type create permissions to be per-bundle optionally and then in that test, give the user only bundle specific permissions?

The last submitted patch, 3: add_a_special_access-2696669-3.patch, failed testing.

mbovan’s picture

Status: Needs work » Needs review
StatusFileSize
new9.3 KB
new8.32 KB

Updates based on the feedback.

There are still test (and non-test) fails when it comes to displaying the list of bundles in case a user has "create {bundle} entities" permission only...

Status: Needs review » Needs work

The last submitted patch, 8: add_a_special_access-2696669-8.patch, failed testing.

mbovan’s picture

Status: Needs work » Needs review
StatusFileSize
new11.8 KB
new2.5 KB

Oops, didn't include EntityTestPermissions file.

Status: Needs review » Needs work

The last submitted patch, 10: add_a_special_access-2696669-10.patch, failed testing.

berdir’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
    @@ -0,0 +1,90 @@
    +/**
    + * Defines an access checker for any entity creation.
    + */
    

    "any entity creation" isn't very specific.

    not sure what a good short description would be. Maybe "for creating an entity of any bundle."'

  2. +++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
    @@ -0,0 +1,90 @@
    +    $entity_type = $route->getRequirement($this->requirementsKey);
    

    $entity_type_id and then you can use $entity_type for the definition.

  3. +++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
    @@ -0,0 +1,90 @@
    +    if (!$entity_type_definition->hasKey('bundle')) {
    +      return $access_control_handler->createAccess(NULL, $account, []);
    

    this should also pass TRUE to return an access result object.

  4. +++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
    @@ -0,0 +1,90 @@
    +    foreach ($this->entityTypeBundleInfo->getBundleInfo($entity_type) as $bundle_id => $bundle_label) {
    

    IIRC, getBundleInfo() returns a bundle_info array. Either name it like that, but since you don't need it anyway, you could also do an array_keys() on getBundleInfo()

  5. +++ b/core/modules/system/src/Tests/Entity/EntityAddUITest.php
    @@ -74,6 +74,25 @@ public function testAddPageWithBundleEntities() {
    +    // Confirm a new user has no permissions to see a new bundle.
    

    // Create a bundle that the user does not have create permissions for?

  6. +++ b/core/modules/system/tests/modules/entity_test/src/EntityTestPermissions.php
    @@ -0,0 +1,75 @@
    +      "create $type_id entities" => array(
    +        'title' => $this->t('%type_name: Create new entity', $type_params),
    +      ),
    +      "edit own $type_id entities" => array(
    +        'title' => $this->t('%type_name: Edit own entity', $type_params),
    +      ),
    +      "edit any $type_id entities" => array(
    +        'title' => $this->t('%type_name: Edit any entity', $type_params),
    +      ),
    +      "delete own $type_id entities" => array(
    +        'title' => $this->t('%type_name: Delete own entity', $type_params),
    +      ),
    +      "delete any $type_id entities" => array(
    +        'title' => $this->t('%type_name: Delete any entity', $type_params),
    

    I would limit it to the permissions we actually need, we can always extend it when necessary.

    Also, not sure if we should get the entity type in there to avoid clashes, especially since it is a test module. E.g. create test entity_test_with_bundle entities or so.

mbovan’s picture

Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new11.12 KB
new4.33 KB
new6.6 KB

Fixed the points above.

It looks like there is another bug which appears when we want to list all bundles (EntityController::addPage())... It could be that EntityAccessControlHandler::checkCreateAccess() checks only for an admin permission and not for "create {bundle}" as well... Also, it might not be possible to know the "create bundle" permissions at that time, not sure...

Here is what I get on /entity_test_with_bundle/add with a user having "create {bundle} entity_test_with_bundle" permission only. Note: there are 2 created bundles.

Edit:

+++ b/core/modules/system/src/Tests/Entity/EntityAddUITest.php
@@ -74,6 +74,25 @@ public function testAddPageWithBundleEntities() {
+      "create test entities",
+      "create test2 entities",

Will update permissions within the next patch.

Status: Needs review » Needs work

The last submitted patch, 13: add_a_special_access-2696669-12.patch, failed testing.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new15.64 KB
new8.45 KB

There is indeed another bug here.

I now finally understand why @mbovan tried to implement access checks on the bundle entity type in the core_crm issue that caused us to find this.

And that's that we check create access for the bundle entity type with the given bundle, instead of the entity type we want to create an entity of. That of course doesn't make any sense and caused it to fail.

After that, there was just one fail remaining. And thinking about this, I finally understand the administer content types permission check in NodeAddAccessCheck.

The question is, what should happen if there are no bundles yet. The test currently expects that you get a page that allows you to create one. But if there's no entity types, we had nothing to check and access was denied.

The answer is to check if the user is allowed to create bundle entities. If so, we allow access. That ties the access check to EntityController::addPage(), though, other pages might not want that behavior, so maybe we need to make it configurable with a special requirement flag?

bojanz’s picture

Title: Add a special access check for default add page route » Incomplete/broken access checking in EntityController::addPage()
Priority: Normal » Major

Retitling.
We have two issues here:
- addPage() checks createAccess() for bundles of the wrong entity type, and the tests aren't complete enough to catch it.
- there is a missing access check to ensure the user can create an entity of at least one bundle, before showing addPage()

The patch fixes both and adds missing test coverage.

Nitpick: EntityTestPermissions is new code, but uses array(). If a reroll happens, let's use [].

The question is, what should happen if there are no bundles yet. The test currently expects that you get a page that allows you to create one. But if there's no entity types, we had nothing to check and access was denied.

The answer is to check if the user is allowed to create bundle entities. If so, we allow access. That ties the access check to EntityController::addPage(), though, other pages might not want that behavior, so maybe we need to make it configurable with a special requirement flag?

That makes sense. I don't see a need for a special flag though, the additional logic feels reasonable.
Let's say I have a content entity form where the bundle is selected with #ajax. If there are no bundles I still wouldn't want a 403, I'd want an opportunity to present my own message/UI logic, and the bundle entity type check makes sense again.

mbovan’s picture

Updated #15 with [] syntax.

Also, extended test to check if we can actually create entities. I would expect that adding "create test entity_test_with_bundle entities" is allowing us to create an entity of that bundle, which doesn't seem to happen?

I now finally understand why @mbovan tried to implement access checks on the bundle entity type in the core_crm issue that caused us to find this.

And that's that we check create access for the bundle entity type with the given bundle, instead of the entity type we want to create an entity of. That of course doesn't make any sense and caused it to fail.

Maybe unrelated, but, what happens with the case when we want to create just an entity, not an entity of the bundle? I guess in that case we would need to check if a user has create access for that specific bundle - what we can check in an access handler for bundle. In crm_core case, ContactTypeAccessControlHandler::checkCreateAccess()?

Edit: Got the explanation from @Berdir, we will not have $entity_bundle in bundle access control handler because... there are no bundles for bundles. :D

mbovan’s picture

The patch.

mbovan’s picture

Issue summary: View changes

Updated the issue summary based on #16.

Status: Needs review » Needs work

The last submitted patch, 18: incomplete_broken-2696669-18.patch, failed testing.

slashrsm’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
    @@ -0,0 +1,107 @@
    +   * The entity manager.
    

    This is entity *type* manager. Super nitpik.

  2. +++ b/core/modules/system/src/Tests/Entity/EntityAddUITest.php
    @@ -74,12 +78,42 @@ public function testAddPageWithBundleEntities() {
    +    $this->clickLink(t('Test label'));
    +    $this->assertResponse(200);
    

    I quickly checked this and it seems we are passing "entity_test_bundle" instead of "test" as $entity_bundle to checkCreateAccess() on access control handler. This is wrong as this isn't id of the bundle but it's entity type id.

    Quickly poking deeper it seems that the value of _entity_create_access on the route might be wrong.

dawehner’s picture

+++ b/core/modules/system/tests/modules/entity_test/src/EntityTestAccessControlHandler.php
@@ -68,7 +69,11 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
+    return AccessResult::allowedIfHasPermissions($account, [
+      'administer entity_test content',
+      'administer entity_test_with_bundle content',
+      'create ' . $entity_bundle . ' entity_test_with_bundle entities',
+    ], 'OR');

Nice, I wasn't aware that this is possible!

Do you think the test is wrong or is there some actual bug in the code still?

mbovan’s picture

Issue summary: View changes

Updated the issue summary with the new bug from #18. Working on a new patch.

mbovan’s picture

Status: Needs work » Needs review
StatusFileSize
new16.86 KB
new1.87 KB

Provided a fix based on @Berdir's proposal on IRC and fixed #21.1.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Oh wow

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 24: incomplete_broken-2696669-24.patch, failed testing.

mbovan’s picture

Status: Needs work » Needs review

Created a followup for link templates in test entities #2699959: Update link templates of test entities

mbovan’s picture

The patch and the interdiff against #24.
The proposed patch/fix doesn't cover the case of EntityTestMul that uses $bundle_key in its add-form link template/route. DefaultHtmlProviderTest should be fixed.

Status: Needs review » Needs work

The last submitted patch, 28: incomplete_broken-2696669-28.patch, failed testing.

berdir’s picture

Status: Needs work » Needs review
StatusFileSize
new19.96 KB
new3.83 KB

This fixes the unit test, the unit test now actually has to use a path that makes sense and there's now always a call getEntityBundleType().

heddn’s picture

+1 on the approach. I ran into this a while back but wasn't able to get my PR merged into entity in time. And this is a much cleaner solution.

bojanz’s picture

Status: Needs review » Reviewed & tested by the community

Looks like we're good to go. I'm happy with punting the general entity_test route cleanup to a followup.

catch’s picture

Issue tags: +rc target

Mostly nits. I don't have good ideas for the naming, feels like we're running into the limit of terminology a bit.

Tagging this as rc target, also leaving it RTBC in case someone else wants to commit it before RC - but probably won't be me.

  1. +++ b/core/core.services.yml
    @@ -1069,6 +1069,11 @@ services:
    +  access_check.entity_create_any:
    

    Not sure about the service name. It's not for creating any entity.

  2. +++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
    @@ -0,0 +1,107 @@
    + * Contains \Drupal\Core\Entity\EntityCreateAnyAccessCheck.
    

    Similarly not sure about the classname - it's not for creating any access checks.

  3. +++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
    @@ -0,0 +1,107 @@
    + * Defines an access checker for creating an entity of any bundle.
    

    Comment isn't 100% clear whether it's any bundle of an entity.

    Or any bundle entity.

  4. +++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
    @@ -0,0 +1,107 @@
    +   * Checks access to create any entity type for the given route.
    

    Now we're not creating entities but entity types.

  5. +++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
    @@ -0,0 +1,107 @@
    +   *   The parametrized route.
    

    typo.

  6. +++ b/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php
    @@ -151,7 +151,11 @@ protected function getAddFormRoute(EntityTypeInterface $entity_type) {
    +      //   test entities have incosistent usage of "add-form" link templates.
    

    inconsistent.

mbovan’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new20.08 KB
new1.9 KB

Fixed #33.4 - #33.6.

berdir’s picture

Status: Needs review » Reviewed & tested by the community

Comment changes look good to me.

wim leers’s picture

Status: Reviewed & tested by the community » Needs review
  1. +++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
    @@ -0,0 +1,107 @@
    +        break;
    

    Why this break? What if a particular bundle returns FORBIDDEN as its access result?

    Back to NW for this.

  2. +++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
    @@ -0,0 +1,107 @@
    +      // If there are no bundles, check if the user is allowed to create new
    +      // ones. If so, allow access, so the add page can show a link to create
    +      // one.
    +      // @see \Drupal\Core\Entity\Controller\EntityController::addPage()
    ...
    +        return $access->orIf($bundle_access_control_handler->createAccess(NULL, $account, [], TRUE));
    

    So say that you don't have any bundles for Node, this is saying that the user will be able to see a "Create node bundle" link?

    I don't see where that is happening in EntityController::addPage()?

berdir’s picture

Status: Needs review » Reviewed & tested by the community

1. Forbidden is fine. Just because one is forbidden doesn't mean that you can't create any others. This isn't combined access for a single thing. It's a real OR, not an AccessResult or.

2.

      // Build the message shown when there are no bundles.
      $link_text = $this->t('Add a new @entity_type.', ['@entity_type' => $bundle_entity_type_label]);
      $link_route_name = 'entity.' . $bundle_entity_type->id() . '.add_form';
      $build['#add_bundle_message'] = $this->t('There is no @entity_type yet. @add_link', [
        '@entity_type' => $bundle_entity_type_label,
        '@add_link' => Link::createFromRoute($link_text, $link_route_name)->toString(),
      ]);

Yes, it exists.

wim leers’s picture

#37:

  1. Ok, that makes sense! Good to have it documented here. Would be even better to have it documented in the code, though that's a nit.
  2. Glad I just missed that.

LGTM.

slashrsm’s picture

+1 for RTBC

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php
@@ -0,0 +1,107 @@
+    // Check whether an entity of any bundle may be created.
+    foreach ($bundles as $bundle) {
+      $access = $access->orIf($access_control_handler->createAccess($bundle, $account, [], TRUE));
+      if ($access->isAllowed()) {
+        break;
+      }
+    }

Given Wim's comment I think we need to improve comment here.

Also I think we have another case to consider. What happens if a user can create bundles but does not has access to the any of the bundles already created?

berdir’s picture

Also I think we have another case to consider. What happens if a user can create bundles but does not has access to the any of the bundles already created?

That would be a new feature and I'd consider that to be highly irrelevant.

This is modelled after node/add and afaik, nobody ever cared about that scenario in the last 10 years :)

alexpott’s picture

With node/add you get access if you have the ability to create bundles regardless of whether you have permissions to create nodes with any of the bundles.

mbovan’s picture

Status: Needs work » Needs review
StatusFileSize
new20.18 KB
new1.84 KB

Updated the comment and the create bundle access check.

berdir’s picture

Status: Needs review » Needs work

Looks good, but we should expand the tests to also cover this.

Create yet another user, do not give him any permissions to create entity_test_with_bundle entities but give him permissions to create entity_test_bundle entities. He should be allowed to access the page and will not see any bundles but should see the message that he can create them.

Might need a change on EntityController::addPage() too, we need to check access before displaying the empty message.

sanja_m’s picture

Status: Needs work » Needs review
StatusFileSize
new20.81 KB
new1.1 KB

Added tests mentioned in #44.

berdir’s picture

Status: Needs review » Reviewed & tested by the community

Great, thanks.

bojanz’s picture

#43 and #44 look good. Let's get it in.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Thanks for addressing the odd edge case. This looks great.

Committed 180191f and pushed to 8.1.x and 8.2.x. Thanks!

diff --git a/core/modules/system/src/Tests/Entity/EntityAddUITest.php b/core/modules/system/src/Tests/Entity/EntityAddUITest.php
index 7d8cf9c..c5fe969 100644
--- a/core/modules/system/src/Tests/Entity/EntityAddUITest.php
+++ b/core/modules/system/src/Tests/Entity/EntityAddUITest.php
@@ -110,9 +110,9 @@ public function testAddPageWithBundleEntities() {
     ]);
     $this->drupalLogin($user);
 
-    // User has access to the add page but no bundles are shown because he does
-    // not have bundle specific permissions, the add bundle message is present
-    // as the user has bundle create permissions.
+    // 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');

I removed the gendered pronoun on commit as per our code documentation standards.

  • alexpott committed 4b26566 on 8.2.x
    Issue #2696669 by mbovan, Berdir, sanja_m: Incomplete/broken access...
jibran’s picture

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

@jibran I think it is best if we don't re-open issues for things like that. Also given the likelihood of re-introducing this codign standard regression I think we should just have a single issue were we re-file patches just to tidy this up so it doesn't become a commit credit thing.

alexpott’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.