From 3ce43cc2b110e110e4d77aff160c7ca012be92e6 Mon Sep 17 00:00:00 2001 From: Eric Smith <ericsmith@catalyst.net.nz> Date: Tue, 4 Feb 2025 09:45:09 +1300 Subject: [PATCH 1/8] Issue #3504114: Add WIP test to show error --- .../NodeAccessCacheRedirectWarning.php | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarning.php diff --git a/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarning.php b/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarning.php new file mode 100644 index 000000000000..461d96f2ba37 --- /dev/null +++ b/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarning.php @@ -0,0 +1,70 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\node\Functional; + +use Drupal\Core\Cache\CacheableMetadata; + +/** + * Tests the node access grants cache context service. + * + * @group node + * @group Cache + */ +class NodeAccessCacheRedirectWarning extends NodeTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['block', 'node_access_test_empty']; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + node_access_rebuild(); + } + + /** + * Quick demonstration of the differences in cache contexts. + * + * The intent here was to visit the nodes to view the error but for whatever + * reason I can't seem to trigger the redirect warning this way. Needs work. + */ + public function testNodeAccessCacheRedirectWarning(): void { + $this->drupalPlaceBlock('local_tasks_block'); + + $this->assertTrue(\Drupal::moduleHandler()->hasImplementations('node_grants')); + + $author = $this->drupalCreateUser([ + 'create page content', + 'edit own page content', + 'view own unpublished content', + ]); + $this->drupalLogin($author); + + $node = $this->drupalCreateNode(['uid' => $author->id(), 'status' => 0]); + + $access = $node->access('view', $author, TRUE); + $unpublished_metadata = CacheableMetadata::createFromObject($access); + + // Reset the node access cache to check the published node. + \Drupal::entityTypeManager()->getAccessControlHandler('node')->resetCache(); + + $node->setPublished(); + $node->save(); + $access = $node->access('view', $author, TRUE); + $published_metadata = CacheableMetadata::createFromObject($access); + + $this->assertSame($unpublished_metadata->getCacheContexts(), $published_metadata->getCacheContexts()); + } + +} -- GitLab From 7c76520ea3264c996ab967687ae63892f26e25af Mon Sep 17 00:00:00 2001 From: Eric Smith <ericsmith@catalyst.net.nz> Date: Thu, 6 Mar 2025 18:08:15 +1300 Subject: [PATCH 2/8] Issue #3504114: If edit own content is added, then the cache context user is used, if edit any permissions then only user.permission is added --- .../NodeAccessCacheRedirectWarning.php | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarning.php b/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarning.php index 461d96f2ba37..5438ed7680c5 100644 --- a/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarning.php +++ b/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarning.php @@ -4,8 +4,6 @@ namespace Drupal\Tests\node\Functional; -use Drupal\Core\Cache\CacheableMetadata; - /** * Tests the node access grants cache context service. * @@ -46,25 +44,29 @@ public function testNodeAccessCacheRedirectWarning(): void { $author = $this->drupalCreateUser([ 'create page content', - 'edit own page content', + 'edit any page content', 'view own unpublished content', ]); $this->drupalLogin($author); $node = $this->drupalCreateNode(['uid' => $author->id(), 'status' => 0]); - $access = $node->access('view', $author, TRUE); - $unpublished_metadata = CacheableMetadata::createFromObject($access); + $this->drupalGet($node->toUrl()); + + $node->setPublished(); + $node->save(); + + $this->drupalGet($node->toUrl()); + + $node->setUnpublished(); + $node->save(); - // Reset the node access cache to check the published node. - \Drupal::entityTypeManager()->getAccessControlHandler('node')->resetCache(); + $this->drupalGet($node->toUrl()); $node->setPublished(); $node->save(); - $access = $node->access('view', $author, TRUE); - $published_metadata = CacheableMetadata::createFromObject($access); - $this->assertSame($unpublished_metadata->getCacheContexts(), $published_metadata->getCacheContexts()); + $this->drupalGet($node->toUrl()); } } -- GitLab From 9ad37e776a53bf5a44398941f231a21e189f86a3 Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Thu, 6 Mar 2025 09:32:11 +0000 Subject: [PATCH 3/8] Rename test. --- ...directWarning.php => NodeAccessCacheRedirectWarningTest.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename core/modules/node/tests/src/Functional/{NodeAccessCacheRedirectWarning.php => NodeAccessCacheRedirectWarningTest.php} (95%) diff --git a/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarning.php b/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarningTest.php similarity index 95% rename from core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarning.php rename to core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarningTest.php index 5438ed7680c5..8946ca75fefe 100644 --- a/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarning.php +++ b/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarningTest.php @@ -10,7 +10,7 @@ * @group node * @group Cache */ -class NodeAccessCacheRedirectWarning extends NodeTestBase { +class NodeAccessCacheRedirectWarningTest extends NodeTestBase { /** * {@inheritdoc} -- GitLab From f9f1f2ba0f4b6534d80a7ce9c50dbb4fc5106846 Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Thu, 6 Mar 2025 10:20:41 +0000 Subject: [PATCH 4/8] Add extra cache context. --- core/modules/node/src/NodeGrantDatabaseStorage.php | 1 + 1 file changed, 1 insertion(+) diff --git a/core/modules/node/src/NodeGrantDatabaseStorage.php b/core/modules/node/src/NodeGrantDatabaseStorage.php index eea6cc100127..9a4c77d2d9ab 100644 --- a/core/modules/node/src/NodeGrantDatabaseStorage.php +++ b/core/modules/node/src/NodeGrantDatabaseStorage.php @@ -121,6 +121,7 @@ public function access(NodeInterface $node, $operation, AccountInterface $accoun // know it for a fact. $set_cacheability = function (AccessResult $access_result) use ($operation) { $access_result->addCacheContexts(['user.node_grants:' . $operation]); + $access_result->addCacheContexts(['user.roles.authenticated']); if ($operation !== 'view') { $access_result->setCacheMaxAge(0); } -- GitLab From 711552bd9e03eed2ddc041f484ffb57ded6e85ce Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Thu, 6 Mar 2025 18:13:14 +0000 Subject: [PATCH 5/8] Move user.roles:authenticated cache context around. --- core/modules/node/src/NodeAccessControlHandler.php | 9 ++++++++- core/modules/node/src/NodeGrantDatabaseStorage.php | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/modules/node/src/NodeAccessControlHandler.php b/core/modules/node/src/NodeAccessControlHandler.php index 963ab53ded41..f4811bdb21dd 100644 --- a/core/modules/node/src/NodeAccessControlHandler.php +++ b/core/modules/node/src/NodeAccessControlHandler.php @@ -207,6 +207,14 @@ protected function checkViewAccess(NodeInterface $node, AccountInterface $accoun // we need to add the node as a cacheable dependency. $cacheability->addCacheableDependency($node); + // Due to the check below, it is not possible to rely only on account + // permissions to determine whether the 'view own unpublished content' + // permission can be checked, instead we also need to check if the user has + // the authenticated role. Just in case anonymous and authenticated users + // are both granted the 'view own unpublished content' permission and also + // have otherwise identical permissions. + $cacheability->addCacheContext(['user.roles.authenticated']); + if ($node->isPublished()) { return NULL; } @@ -216,7 +224,6 @@ protected function checkViewAccess(NodeInterface $node, AccountInterface $accoun return NULL; } - $cacheability->addCacheContexts(['user.roles:authenticated']); // The "view own unpublished content" permission must not be granted // to anonymous users for security reasons. if (!$account->isAuthenticated()) { diff --git a/core/modules/node/src/NodeGrantDatabaseStorage.php b/core/modules/node/src/NodeGrantDatabaseStorage.php index 9a4c77d2d9ab..eea6cc100127 100644 --- a/core/modules/node/src/NodeGrantDatabaseStorage.php +++ b/core/modules/node/src/NodeGrantDatabaseStorage.php @@ -121,7 +121,6 @@ public function access(NodeInterface $node, $operation, AccountInterface $accoun // know it for a fact. $set_cacheability = function (AccessResult $access_result) use ($operation) { $access_result->addCacheContexts(['user.node_grants:' . $operation]); - $access_result->addCacheContexts(['user.roles.authenticated']); if ($operation !== 'view') { $access_result->setCacheMaxAge(0); } -- GitLab From 025c6e98df936d134ec599b031133cfdbab9515b Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Thu, 6 Mar 2025 20:21:50 +0000 Subject: [PATCH 6/8] Add the node grants cache context in exactly one additional situation. --- core/modules/node/src/NodeAccessControlHandler.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/core/modules/node/src/NodeAccessControlHandler.php b/core/modules/node/src/NodeAccessControlHandler.php index f4811bdb21dd..2ee7d4965d20 100644 --- a/core/modules/node/src/NodeAccessControlHandler.php +++ b/core/modules/node/src/NodeAccessControlHandler.php @@ -213,12 +213,12 @@ protected function checkViewAccess(NodeInterface $node, AccountInterface $accoun // the authenticated role. Just in case anonymous and authenticated users // are both granted the 'view own unpublished content' permission and also // have otherwise identical permissions. - $cacheability->addCacheContext(['user.roles.authenticated']); if ($node->isPublished()) { return NULL; } $cacheability->addCacheContexts(['user.permissions']); + $cacheability->addCacheContexts(['user.roles:authenticated']); if (!$account->hasPermission('view own unpublished content')) { return NULL; @@ -230,7 +230,17 @@ protected function checkViewAccess(NodeInterface $node, AccountInterface $accoun return NULL; } + // When access is granted due to the 'view own unpublished content' + // permission and for no other reason, node grants are bypassed. However, + // to ensure the full set of cacheable metadata is available to variation + // cache, additionally add the node_grants cache context so that if the + // status or the owner of the node changes, cache redirects will continue to + // reflect the latest state without needing to be invalidated. $cacheability->addCacheContexts(['user']); + if ($this->moduleHandler->hasImplementations('node_grants')) { + $cacheability->addCacheContexts(['user.node_grants:view']); + } + if ($account->id() != $node->getOwnerId()) { return NULL; } -- GitLab From e4be93b0d7a393b80abc3662dce11e93f97ada9b Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Thu, 6 Mar 2025 20:57:58 +0000 Subject: [PATCH 7/8] Move things back where they were, but keep comment. --- .../node/src/NodeAccessControlHandler.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/modules/node/src/NodeAccessControlHandler.php b/core/modules/node/src/NodeAccessControlHandler.php index 2ee7d4965d20..743f614e61c8 100644 --- a/core/modules/node/src/NodeAccessControlHandler.php +++ b/core/modules/node/src/NodeAccessControlHandler.php @@ -207,23 +207,23 @@ protected function checkViewAccess(NodeInterface $node, AccountInterface $accoun // we need to add the node as a cacheable dependency. $cacheability->addCacheableDependency($node); - // Due to the check below, it is not possible to rely only on account - // permissions to determine whether the 'view own unpublished content' - // permission can be checked, instead we also need to check if the user has - // the authenticated role. Just in case anonymous and authenticated users - // are both granted the 'view own unpublished content' permission and also - // have otherwise identical permissions. - if ($node->isPublished()) { return NULL; } $cacheability->addCacheContexts(['user.permissions']); - $cacheability->addCacheContexts(['user.roles:authenticated']); if (!$account->hasPermission('view own unpublished content')) { return NULL; } + // Due to the check below, it is not possible to rely only on account + // permissions to determine whether the 'view own unpublished content' + // permission can be checked, instead we also need to check if the user has + // the authenticated role. Just in case anonymous and authenticated users + // are both granted the 'view own unpublished content' permission and also + // have otherwise identical permissions. + $cacheability->addCacheContexts(['user.roles:authenticated']); + // The "view own unpublished content" permission must not be granted // to anonymous users for security reasons. if (!$account->isAuthenticated()) { -- GitLab From 916ff04d83093ead8482698d60b77e788acba911 Mon Sep 17 00:00:00 2001 From: catch <6915-catch@users.noreply.drupalcode.org> Date: Thu, 6 Mar 2025 21:09:45 +0000 Subject: [PATCH 8/8] Alternate approach - merge the access results. --- core/modules/node/src/NodeAccessControlHandler.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/modules/node/src/NodeAccessControlHandler.php b/core/modules/node/src/NodeAccessControlHandler.php index 743f614e61c8..11561df302fe 100644 --- a/core/modules/node/src/NodeAccessControlHandler.php +++ b/core/modules/node/src/NodeAccessControlHandler.php @@ -131,12 +131,11 @@ protected function checkAccess(EntityInterface $node, $operation, AccountInterfa assert($node instanceof NodeInterface); $cacheability = new CacheableMetadata(); + $view_access_result = NULL; + /** @var \Drupal\node\NodeInterface $node */ if ($operation === 'view') { - $result = $this->checkViewAccess($node, $account, $cacheability); - if ($result !== NULL) { - return $result; - } + $view_access_result = $this->checkViewAccess($node, $account, $cacheability); } [$revision_permission_operation, $entity_operation] = static::REVISION_OPERATION_MAP[$operation] ?? [ @@ -185,6 +184,9 @@ protected function checkAccess(EntityInterface $node, $operation, AccountInterfa $access_result = $this->grantStorage->access($node, $operation, $account); if ($access_result instanceof RefinableCacheableDependencyInterface) { $access_result->addCacheableDependency($cacheability); + if ($view_access_result) { + $access_result->addCacheableDependency($view_access_result); + } } return $access_result; } -- GitLab