Problem/Motivation

All Paragraphs entities are embedded in another entity. When someone manages to directly access the entity, the default paragraphs access check in ParagraphAccessControlHandler will always return AccessResult::allowed().

Example:
When you add a file field to a paragraph type, and set the storage to private, Drupal will automatically see which entities referenced to the file when someone requests it. Drupal will execute a view access check on the referencing entity, and paragraphs will return AccessResult::allowed().

Proposed resolution

We need to have a more advanced access check besides relying on the parent entity to handle access control.
In D7 we always keep track of our host entity, and if we can't find it (not set by widget/formatter), we'll load it.
If we do the same for D8, we are always able to check access on our parent entity.

Remaining tasks

  • Track host entity, if not found, try to find it by reverse lookup and load it
  • Check host entity for access. Code can be based on D7 version
  • Create upgrade path
  • Create test for specific problem (file field)
CommentFileSizeAuthor
#83 paragraphs_2585447_access_83.patch7.33 KBmiro_dietiker
#74 interdiff-2585447-56-74.txt2.08 KBjohnchque
#74 entity_access_does_not-2585447-74-non-combined.patch7.32 KBjohnchque
#74 entity_access_does_not-2585447-74-combined.patch24.76 KBjohnchque
#63 not-combined-2585447-63.patch7.5 KBtduong
#63 not-combined-2585447-63-test_only.patch4.16 KBtduong
#63 interdiff-2585447-not-combined-56-63.txt1.51 KBtduong
#56 interdiff-2585447-47-56.txt873 bytesjohnchque
#56 entity_access_does_not-2585447-56-combined.patch22.35 KBjohnchque
#56 entity_access_does_not-2585447-56-non-combined.patch6.63 KBjohnchque
#54 interdiff-2585447-35-39.txt727 bytesjohnchque
#47 interdiff-2585447-23-46.txt3 KBjohnchque
#47 entity_access_does_not-2585447-46-non-combined.patch6.54 KBjohnchque
#47 entity_access_does_not-2585447-46-combined.patch22.27 KBjohnchque
#39 interdiff-2585447-23-39.txt3 KBjohnchque
#39 entity_access_does_not-2585447-39-non-combined.patch6.54 KBjohnchque
#39 entity_access_does_not-2585447-39-combined.patch22.26 KBjohnchque
#35 combined-test-patch.patch22.35 KBjohnchque
#28 entity_access_does_not-2585447-28-combined.patch22.29 KBjohnchque
#28 interdiff-2585447-23-28.txt2.69 KBjohnchque
#28 entity_access_does_not-2585447-28-non-combined.patch6.56 KBjohnchque
#23 entity_access_does_not-2585447-23.patch6.05 KBjohnchque
#18 entity_access_does_not-2585447-18.patch21.78 KBjohnchque
#12 interdiff-3-12.txt5.8 KBjeroen.b
#12 paragraphs-host-entity-access-2585447-12.patch20.76 KBjeroen.b
#3 paragraphs-host-entity-access-2585447-3.patch21.13 KBjeroen.b

Comments

jeroen.b created an issue. See original summary.

jeroen.b’s picture

Issue summary: View changes
jeroen.b’s picture

Here's a first attempt, it addresses all remaining tasks.
There is a new issue over at Entity Reference Revisions that makes sure we don't load the host entity on rendering for performance reasons #2586771: Add referring entity in the formatter so the entity can use it.
I simpletested this locally with the Entity Reference Revisions patch and without.

jeroen.b’s picture

Status: Active » Needs review
jeroen.b’s picture

Assigned: jeroen.b » Unassigned
miro_dietiker’s picture

Hmm, this direction feels kinda wrong to me.

Suddenly the target entity needs to know about the embedding context and even persist it.
As a result, you need to fight already against serialization recursion problems..

I thought we should resolve the permission problem on the field level (by the entity reference revisions field / the paragraphs formatter) and suppress items top down. What is the problem that requires the embedded entity itself to be aware of the parent context?

jeroen.b’s picture

Here is an example of why we need to do this:

When you add a file field to a paragraph type, and set the storage to private, Drupal will automatically see which entities referenced to the file when someone requests it. Drupal will execute a view access check on the referencing entity, and paragraphs will return AccessResult::allowed().

I know it feels wrong, but sadly, it's the only proper way to do access control.
If we want to do field level access checking we still need to know about the host entity and the host field so the solution would be kind of the same.

As a result, you need to fight already against serialization recursion problems..

Can't really say I'm fighting it. I'm just removing it as we only use hostEntity as a "cache" and it's not part of the actual entity.

I thought we should resolve the permission problem on the field level (by the entity reference revisions field / the paragraphs formatter) and suppress items top down.

There is currently no issue in the top down approach, it already works fine.
The only issue this takes care of is direct entity access checking on paragraphs entities.

miro_dietiker’s picture

Hm, i think the problem originates in the fact that Drupal does not offer anything to deal with embedding / recursion.

One assumption we introduced was that direct access is not happening ever. To make sure this applies, we did not offer a link template to edit the view and entity directly.

I don't know what we can do with the private file problem using the core methods.
Should we try to report usage against the host entity?

I will ping Berdir for some better advice about this challenging situation.

jeroen.b’s picture

Thanks!
I'm not sure if private files are the only problem, I can image there are other modules doing direct entity access.

jeroen.b’s picture

One other approach I thought of:
Copying the file usage service concept to entity reference revisions to create a entity reference revisions usage service. It would still require most of this patch but it's a more general fix.
That might alo be the start of a fix for #2429335: Deletion of referenced entities

berdir’s picture

Hm, don't have much to add.

Usually, we say that it is not your responsibility to check access of your parent, e.g. it is wrong for field level access checks to check the entity too.

But yes, the private file use case is valid, we have to do something about that. The usage approach might work, we've discussed that before, also with drupal-media/media_entity, which has similar requirements.

Reviewed parts of the patch:

  1. +++ b/src/Entity/Paragraph.php
    @@ -303,4 +333,105 @@ class Paragraph extends ContentEntityBase implements ParagraphInterface {
    +            // Little trick to get the revision id.
    +            $query = \Drupal::entityQueryAggregate($host_entity_type)
    +              ->groupby($revision_id_key)
    +              ->condition($host_entity_field_name . '.target_id', $this->id())
    +              ->condition($host_entity_field_name . '.target_revision_id', $this->getRevisionId())
    +              ->allRevisions()
    +              ->range(0, 1);
    +
    +            $host_ids = $query->execute();
    +            if (count($host_ids)) {
    +              $host_id = reset($host_ids);
    

    the revision id is always the array key of the result. I don't think you really want allRevisions() here, you care about the current revision that's using oyur paragraph, no?

  2. +++ b/src/ParagraphAccessControlHandler.php
    @@ -25,16 +26,87 @@ class ParagraphAccessControlHandler extends EntityAccessControlHandler
    +    // If parent says forbidden, don't even try anything else.
    +    if (parent::checkAccess($entity, $operation, $account)->isForbidden()) {
    +      return AccessResult::forbidden()->cacheUntilEntityChanges($entity);
    +    }
    +
    +    // If parent says allowed, don't even try anything else.
    +    if (parent::checkAccess($entity, $operation, $account)->isAllowed()) {
    +      return AccessResult::allowed()->cacheUntilEntityChanges($entity);
    +    }
    +
    

    The thing with access result objects is that you should keep using them, not re-create them.

    So I think you want something like

    $result = parent:...
    if (!$result->isNeutral()) {
      return $result->addCacheableDependency($entity);
    

    (cacheUntilEntityCHanges is deprecated).

  3. +++ b/src/ParagraphAccessControlHandler.php
    @@ -25,16 +26,87 @@ class ParagraphAccessControlHandler extends EntityAccessControlHandler
    +      // Check access on host entity.
    +      if ($host_entity->access($check_parent_operation, $account)) {
    +        return AccessResult::allowed()->cacheUntilEntityChanges($entity)->cacheUntilEntityChanges($host_entity);
    +      } else {
    +        return AccessResult::forbidden()->cacheUntilEntityChanges($entity)->cacheUntilEntityChanges($host_entity);
    +      }
    

    Same here, you want to use the existing access object, maybe that varies by user or by permission (or moon phase ;)). Just do return $host_entity->access($check_parent_operation, $account, TRUE)->addCacheableDependency($entity);

    I don't think you need a dependency on the host entity, if it depends on a status of that (e.g. published or not), then the host entity access control handler is responsible for adding that.

jeroen.b’s picture

Thanks for the review, fixed in this patch.

I don't think you really want allRevisions() here, you care about the current revision that's using oyur paragraph, no?

I need allRevisions() to make sure we find the reference, even if it is a older revision. Let's say you have revisions enabled on your node. You upload a private file to a pragraph, save the node, remove the file again, save the node again.
Normal users should be denied access to the deleted file, but when the editor looks at the older revision, he should still be able to see the file. This is the only way to find the correct revision right?

But yes, the private file use case is valid, we have to do something about that. The usage approach might work, we've discussed that before, also with drupal-media/media_entity, which has similar requirements.

How about we get this patch in now and work on the usage approach in a Entity Reference Revisions issue? The change in this module would only be small if we implement that:

  • Deprecate / empty old update functions
  • Remove host_entity_type and host_entity_field_name fields
  • Remove reverse lookup on fetchHostDetails()
  • Update ParagraphAccessControlHandler to implement usage
jeroen.b’s picture

Can we talk about this again? I consider this module insecure while this is issue isopen.

miro_dietiker’s picture

OK, found time to discuss the situation.

We decided that the proper approach for paragraphs is to declare a paragraph as an embedded entity by concept.
This means that the entity host is immutable and due to the field API design it's needed to resolve the parent relationship from the item. And the access check by paragraphs is needed and thus (with knowing that there is only one parent) simple.

We still want to see integrity management delegated as much as possible to entity reference revisions:
ERR introduces the concept of a composite entity. It will support parent_type/parent_id keys in the annotation of the target entity that define the parent entity type and id field name. ERR will then make sure that the IDs are persisted on creation, and also will delete child items.

This means that a paragraph item is NOT reusable (thus has no label, no management metadata). If ever someone wants to do this, he will add a module with a new / altered widget that could still add an operation "make this paragraph item reusable" that leads to creation of a separate (reusable, value cloned) entity with own metadata and switching the paragraphs bundle to a bundle with a reference to the reusable item. Integrity (deleting the reference) is still well done by ERR... And all of the complexity of a mixed case disappears.

Status: Needs review » Needs work

The last submitted patch, 12: paragraphs-host-entity-access-2585447-12.patch, failed testing.

miro_dietiker’s picture

Priority: Major » Critical
Status: Needs work » Postponed

So let's postpone this issue for proper ERR support #2641824: Maintain composite relationship.
And promote to release critical for the security implications of potential information disclosure.

johnchque’s picture

Assigned: Unassigned » johnchque
Status: Postponed » Active

It seems the Composite relationship will help with this.

johnchque’s picture

Status: Active » Needs review
StatusFileSize
new21.78 KB

Discussed with @Berdir , added host check, the patch includes test coverage. Also I am working on top of the issue #2676190: Make paragraphs use ERR composite relationship since that will make the check easier.

Status: Needs review » Needs work

The last submitted patch, 18: entity_access_does_not-2585447-18.patch, failed testing.

The last submitted patch, 18: entity_access_does_not-2585447-18.patch, failed testing.

The last submitted patch, 18: entity_access_does_not-2585447-18.patch, failed testing.

johnchque’s picture

Seems to be working locally, maybe the tests will pass after have committed the other issue (?)

johnchque’s picture

Status: Needs work » Needs review
StatusFileSize
new6.05 KB

Sorry, I should also have uploaded the non combined patch. Here we go.

Status: Needs review » Needs work

The last submitted patch, 23: entity_access_does_not-2585447-23.patch, failed testing.

The last submitted patch, 23: entity_access_does_not-2585447-23.patch, failed testing.

The last submitted patch, 23: entity_access_does_not-2585447-23.patch, failed testing.

miro_dietiker’s picture

+++ b/src/ParagraphAccessControlHandler.php
@@ -22,9 +22,15 @@ class ParagraphAccessControlHandler extends EntityAccessControlHandler {
+  protected function checkAccess(EntityInterface $paragraph, $operation, AccountInterface $account) {
...
+    if (!$paragraph->isNew()) {
+      $parent_access = $paragraph->getParentEntity()->access($operation, $account, TRUE);

There is a moment where a paragraph is stored and not yet attached to the parent entity. Thus getParentEntity() will not return the parent.

Your code would fail hard here.

Since this situation is special, it contains risk for many other applications to break things. We should well document it and test cover it.
Actually this situation is initiated by the ERR module and not paragraphs and needs documentation there if not already done.

johnchque’s picture

Status: Needs work » Needs review
StatusFileSize
new6.56 KB
new2.69 KB
new22.29 KB

Uploaded combined and non combined patches, add a check for the parent and also extended tests for preview mode.

The last submitted patch, 28: entity_access_does_not-2585447-28-non-combined.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 28: entity_access_does_not-2585447-28-combined.patch, failed testing.

The last submitted patch, 28: entity_access_does_not-2585447-28-non-combined.patch, failed testing.

The last submitted patch, 28: entity_access_does_not-2585447-28-non-combined.patch, failed testing.

The last submitted patch, 28: entity_access_does_not-2585447-28-combined.patch, failed testing.

The last submitted patch, 28: entity_access_does_not-2585447-28-combined.patch, failed testing.

johnchque’s picture

Status: Needs work » Needs review
StatusFileSize
new22.35 KB

Lets see why the combined patch is not working.

Status: Needs review » Needs work

The last submitted patch, 35: combined-test-patch.patch, failed testing.

The last submitted patch, 35: combined-test-patch.patch, failed testing.

The last submitted patch, 35: combined-test-patch.patch, failed testing.

johnchque’s picture

Status: Needs work » Needs review
StatusFileSize
new22.26 KB
new6.54 KB
new3 KB

Made small changes, added interdiff against patch of comment 23 to make the review easier.

The last submitted patch, 39: entity_access_does_not-2585447-39-combined.patch, failed testing.

The last submitted patch, 39: entity_access_does_not-2585447-39-combined.patch, failed testing.

The last submitted patch, 39: entity_access_does_not-2585447-39-combined.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 39: entity_access_does_not-2585447-39-non-combined.patch, failed testing.

The last submitted patch, 39: entity_access_does_not-2585447-39-non-combined.patch, failed testing.

The last submitted patch, 39: entity_access_does_not-2585447-39-non-combined.patch, failed testing.

tduong’s picture

+++ b/src/Tests/ParagraphsAccessTest.php
@@ -0,0 +1,132 @@
+    $image_url = file_create_url('private://privateImage.jpg');

Debugging it I get http://d8.dev/system/files/privateImage.jpg, but form raw data I get /system/files/styles/medium/private/privateImage.jpg?itok=<em>something</em>, so maybe you should hardcode it. Not sure if this could work though.
(I had a similar problem here #2671046: Paragraphs Type Permission should take the publish/unpublish status into account and I've just hardcoded it).

johnchque’s picture

Status: Needs work » Needs review
StatusFileSize
new22.27 KB
new6.54 KB
new3 KB

Changed the test, this should work, also again diff against patch #23

The last submitted patch, 47: entity_access_does_not-2585447-46-combined.patch, failed testing.

The last submitted patch, 47: entity_access_does_not-2585447-46-combined.patch, failed testing.

The last submitted patch, 47: entity_access_does_not-2585447-46-combined.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 47: entity_access_does_not-2585447-46-non-combined.patch, failed testing.

The last submitted patch, 47: entity_access_does_not-2585447-46-non-combined.patch, failed testing.

The last submitted patch, 47: entity_access_does_not-2585447-46-non-combined.patch, failed testing.

johnchque’s picture

StatusFileSize
new727 bytes

Added interdiff against #35

tduong’s picture

I don't agree that changing that $image_url position could change anything for the test, since you are creating that file URL hardcoding the URI...
Also, please write "\Drupal\paragraphs\Tests\ParagraphsAccessTest"! ;)

johnchque’s picture

Status: Needs work » Needs review
StatusFileSize
new6.63 KB
new22.35 KB
new873 bytes

Changes made based on comment #55 also changed a bit the generation of the url, let's see what testbot thinks about this.

The last submitted patch, 56: entity_access_does_not-2585447-56-non-combined.patch, failed testing.

The last submitted patch, 56: entity_access_does_not-2585447-56-non-combined.patch, failed testing.

The last submitted patch, 56: entity_access_does_not-2585447-56-non-combined.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 56: entity_access_does_not-2585447-56-combined.patch, failed testing.

The last submitted patch, 56: entity_access_does_not-2585447-56-combined.patch, failed testing.

The last submitted patch, 56: entity_access_does_not-2585447-56-combined.patch, failed testing.

tduong’s picture

Status: Needs work » Needs review
StatusFileSize
new1.51 KB
new4.16 KB
new7.5 KB

For the non-combined patch, you haven't defined 'parent_type' and 'parent_id' anywhere, that is why all the tests fail... Added these data, works locally, let's see if there are any other bug for testbot.
Later we will debug deeper the combined patch.

The last submitted patch, 63: not-combined-2585447-63-test_only.patch, failed testing.

The last submitted patch, 63: not-combined-2585447-63-test_only.patch, failed testing.

berdir’s picture

The non-combined patch isn't meant to work, it's just for reviewing. those fields are added in the other issue.

When you have something like that, I suggest you disable testing for those.

The last submitted patch, 63: not-combined-2585447-63-test_only.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 63: not-combined-2585447-63.patch, failed testing.

The last submitted patch, 63: not-combined-2585447-63.patch, failed testing.

The last submitted patch, 63: not-combined-2585447-63.patch, failed testing.

johnchque’s picture

Thanks for the feedback, after checking the test fail it seems it is not working, it still finds the image, not sure how to proceed.

tduong’s picture

Status: Needs work » Needs review

Need help for this.

johnchque’s picture

Debugging some I think I found the problem, should be in the access check.

+++ b/src/ParagraphAccessControlHandler.php
@@ -22,9 +22,15 @@ class ParagraphAccessControlHandler extends EntityAccessControlHandler {
+    /** @var \Drupal\paragraphs\Entity\Paragraph $paragraph */
+    if ($paragraph->getParentEntity() != NULL) {
+      $parent_access = $paragraph->getParentEntity()->access($operation, $account, TRUE);
+      return AccessResult::allowedIf($operation != 'view' || $paragraph->status->value)
+        ->andIf($parent_access);
+    }
+    return AccessResult::allowedIf($operation != 'view' || $paragraph->status->value);

Will test more and upload a new patch soon.

johnchque’s picture

It seems there would be a caching problem in core. Added a second image inside the same paragraph, the access check should be fine. Let's see again what test bot thinks about this.

The last submitted patch, 74: entity_access_does_not-2585447-74-combined.patch, failed testing.

The last submitted patch, 74: entity_access_does_not-2585447-74-combined.patch, failed testing.

The last submitted patch, 74: entity_access_does_not-2585447-74-combined.patch, failed testing.

johnchque’s picture

After so many ideas, changes and patches 'the idea' came to us, it seems testbot is using rc4 of ERR which does not implement composite relationship yet. That is the reason why the tests are failing.

miro_dietiker’s picture

Still applies, retesting. :-)

The last submitted patch, 74: entity_access_does_not-2585447-74-combined.patch, failed testing.

The last submitted patch, 74: entity_access_does_not-2585447-74-combined.patch, failed testing.

The last submitted patch, 74: entity_access_does_not-2585447-74-combined.patch, failed testing.

miro_dietiker’s picture

StatusFileSize
new7.33 KB

Ah, we need the second patch.

miro_dietiker’s picture

Status: Needs review » Fixed

Another christmas present today. Committing!

Status: Fixed » Closed (fixed)

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