Problem/Motivation

The node access grant system does not add a cacheable dependency on nodes in all circumstances. It's important that this dependency be added because variations to the node can most certainly have an affect on the node access query logic.

Proposed resolution

Add the missing cache dependency!

Original report

A content type has an entity reference field.
A node "foo" of that content type references an unpublished node "bar".
Viewing the node "foo" as an anonymous user will create caches.
When publishing the node "bar" it is expected to become visible on the page "foo" but that page won't show the referenced node "bar" that is now published.

EntityReferenceEntityFormatter::viewElements only adds accessible nodes to the render array (not the unpublished or inaccessible ones) but a node that is referenced might be inaccessible at first and become accessible later.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mpp created an issue. See original summary.

mpp’s picture

Status: Active » Needs review
FileSize
1.27 KB

Status: Needs review » Needs work

The last submitted patch, 2: 2946073_2.patch, failed testing. View results

mpp’s picture

Status: Needs work » Needs review
FileSize
1.32 KB
Berdir’s picture

The formatter adds cacheability metadata to the render array. The access implementation is responsible for adding the entity as a cacheable dependency to the access result if changing it can result in a change of the access result.

Node is doing that, are you sure you really had this problem with nodes and not with another entity type?

mpp’s picture

Hi Berdir, thanks for the follow up.

I'm quite sure the problem occurs with nodes. It only works when I explicitly add the tags in viewElements().

I assume you refer to the code in EntityReferenceFormatterBase::getEntitiesToView:

        // Add the access result's cacheability, ::view() needs it.
        $item->_accessCacheability = CacheableMetadata::createFromObject($access);

And in EntityReferenceFormatterBase::view:

    // Apply the cacheability metadata for the inaccessible entities and the
    // entities for which the corresponding render subtree could not be found.
    // This causes the field to be rendered (and cached) according to the cache
    // contexts by which the access results vary, to ensure only users with
    // access to this field can view it. It also tags this field with the cache
    // tags on which the access results depend, to ensure users that cannot view
    // this field at the moment will gain access once any of those cache tags
    // are invalidated.
    $field_level_access_cacheability->merge(CacheableMetadata::createFromRenderArray($elements))
      ->applyTo($elements);

I find it peculiar that the EntityReferenceLabelFormatter explicitly adds the tags:

      $elements[$delta]['#cache']['tags'] = $entity->getCacheTags();

But only for the accessible (published) terms:

    foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) {

While I'd expect the tags of the inaccessible entities to be added to the field ($elements array) itself.

These are the cache tags returned by ContentEntityBase::access():

[
   'cacheContexts' => [
    0 => 'url.query_args:key',
    1 => 'user.node_grants:view',
    2 => 'user.permissions',
  ],
   'cacheTags' => [],
   'cacheMaxAge' => -1,
]

I don't see anything here that could invalidate the entity reference field when a referenced node gets published?

mpp’s picture

mpp’s picture

Berdir’s picture

Component: cache system » node system
Status: Needs review » Needs work

I just recently wrote tests for paragraph (library) that relies on that working: #2943601: Make Paragraphs Library items publishable.

I also tested it manually and it works fine for me on a standard D8 site with a reference field on page.

But I just noticed a problem in \Drupal\node\NodeAccessControlHandler::checkAccess(), you can see that it adds the addCacheableDependency() call but only *if* it grants access.

If access isn't allowed, it continues to the grant storage, and then in \Drupal\node\NodeGrantDatabaseStorage::access() does it correctly:

return AccessResult::allowedIf($node->isPublished())->addCacheableDependency($node);

It always adds the cacheable dependency, but only if you are not using node grants. So my wild guess would be that you are using access grants?

So there is a problem, but it's a node specific problem of not always adding the cacheable dependency on the node. Not a problem with entity reference formatters or even the cache system. Whether or not the cache tag needs to be added depends on the access logic. E.g. for entity types without a status, saving them can't change whether you have access or not, so we don't want to add unnecessary cache tags.

timmillwood’s picture

Issue tags: +Needs tests
mpp’s picture

Hi @berdir, you're right we have the view_unpublished module enabled:

/**
 * Implements hook_node_grants().
 */
function view_unpublished_node_grants(AccountInterface $account, $op) {

I added a patch that should fix it in the node system or do you consider it the responsability of view_unpublished to add the cacheable metadata?

Triggering the testbot. If we want to test this in the node system I assume we can do so in NodeAccessAutoBubblingTest::testNodeAccessCacheabilitySafeguard?

Berdir’s picture

Yes, I agree it should be fixed in the node module. I'd try to do it in NodeAccessControlHandler, possibly by merging multiple access results together, like I did in the referenced paragraphs issue.

I'll also ping Wim about this.

mpp’s picture

After some further debugging I noticed that the cacheMaxAge is always -1 for these nodes so my last patch will completely disable caching.
I have to admit I'm digging deeper here than usual but it's been very educational :-)

I had hoped to meet Wim at this event (https://www.meetup.com/DUG-BE/events/247580511/) but I see he hasn't registered (yet *wink*).

remkoklein’s picture

We have the same issue with view_unpublished enabled. It appears to be an issue when the hook 'hook_node_grants' is used. At that point the access function in NodeGrantDatabaseStorage doesn't add the node as cacheable dependency anymore.

I've used patch #11 and it appears to be adding the correct tags to the cache entries and it works!

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

mpp’s picture

Issue summary: View changes
mpp’s picture

Version: 8.6.x-dev » 8.7.x-dev

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

bkosborne’s picture

Title: Cache tags are missing for unpublished entity references » Node grant access check missing cacheable dependency on node
Issue summary: View changes

Updating title and summary to better reflect what's going on here. I also came across this bug when debugging cacheability issues related to the access denied exception cacheability data missing the node:[nid] cache tag. This is the cause.

bkosborne’s picture

Status: Needs work » Needs review

I don't understand the comment in #13, but this patch shouldn't cause all your nodes to become uncacheable? The patch in #13 looks correct to me. So this just needs a regression test I think.

bkosborne’s picture

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

neclimdul’s picture

#11 looks functionally fine but Berdir's suggestions make sense. I'm not really clear _how_ he was suggesting it though. Maybe this?

claudiu.cristea’s picture

Here's a test that acts also as interdiff.

The last submitted patch, 24: 2946073-24-test-only.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

claudiu.cristea’s picture

FileSize
3.75 KB
819 bytes

Fix coding standards issue.

claudiu.cristea’s picture

Issue tags: +D8 cacheability

Note that node_test module is enabled in test because is implementing hook_node_grants().

idimopoulos’s picture

I also noticed the node_test. Thanks for clearing that out @claudiu.cristea. However, I think we should have a comment in the $modules array since if someone comes across it and sees it as a needless dependency and removes it, the test will still be green without it but will be a false positive.

claudiu.cristea’s picture

FileSize
948 bytes
4.01 KB

@idimopoulos that is a good remark. In fact I want that this test fails if someone is removing that module. Better that any explanation.

claudiu.cristea’s picture

FileSize
915 bytes
4.05 KB

Improving the comment.

idimopoulos’s picture

Status: Needs review » Reviewed & tested by the community

The test provides good cover and the fix is proved. Moving to RTBC.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

catch’s picture

Version: 9.1.x-dev » 8.9.x-dev
Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/node/src/NodeAccessControlHandler.php
@@ -104,7 +105,11 @@ protected function checkAccess(EntityInterface $node, $operation, AccountInterfa
 
     // Evaluate node grants.
-    return $this->grantStorage->access($node, $operation, $account);
+    $access_result = $this->grantStorage->access($node, $operation, $account);
+    if ($operation === 'view' && $access_result instanceof RefinableCacheableDependencyInterface) {
+      $access_result->addCacheableDependency($node);
+    }
+    return $access_result;
   }
 

This could use a comment to explain why we only add the cacheable dependency on the view operation and not the edit or delete operations.

Also moving the version back to 8.9.x since this could go into a patch release.

claudiu.cristea’s picture

Status: Needs work » Needs review
FileSize
1.02 KB
4.42 KB

Documented.

idimopoulos’s picture

Status: Needs review » Reviewed & tested by the community

Setting this as RTBC again then if everyone is ok.

  • catch committed 6685ea2 on 9.1.x
    Issue #2946073 by claudiu.cristea, mpp, neclimdul, Berdir, idimopoulos,...

  • catch committed e0712d2 on 9.0.x
    Issue #2946073 by claudiu.cristea, mpp, neclimdul, Berdir, idimopoulos,...

  • catch committed 4732d9f on 8.9.x
    Issue #2946073 by claudiu.cristea, mpp, neclimdul, Berdir, idimopoulos,...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 9.0.x and cherry-picked back to 8.9.x, thanks!

Status: Fixed » Closed (fixed)

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