Problem/Motivation

Access cacheability is not correct when the "view own unpublished content" is in use, leading to improperly cached render arrays.

Steps to reproduce

(See even more minimalist reproduction steps is MR !8198)

1. Standard install
2. Add an entity reference field to the Page content type called "Related Articles" where article content can be referenced.
3. Configure the "Related Articles" field to display as a rendered entity.
4. Create Content Editor named "Dan"
5. Log in as Dan
6. Create an Article named "Dan's Article".
7. Create a Page named "Test Page" and add "Dan's Article" as a Related Article.
8. As the admin, unpublish "Dan's Article"
9. As Dan, View "Test Page". You will see "Dan's Article" rendered in pink. Good.
10. Create a new Content Editor named Flan.
11. Log in as Flan.
12. As Flan, view "Test Page". You will NOT see "Dan's Article". Good.
13. Clear Caches.
14. As Flan, view "Test Page". You will NOT see "Dan's Article". Good.
15. As Dan, view "Test Page". You will NOT see "Dan's Article". This is not correct.

Note that you will never see MORE than you are supposed to see. This is not an access bypass problem. Rather you will potentially see less than you are supposed to see.

In this particular case, the incorrect cacheable metadata is being created within EntityReferenceFormatterBase::getEntitiesToView:

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

Proposed resolution

Bubble up user cache context when there is no other option, since the lack of proper cache context on the final render result causes this problem.

Remaining tasks

  1. Fix #2973356: Cacheability information from route access checker access results are ignored by dynamic_page_cache because it is currently a blocker of fixing this one, see more details in https://git.drupalcode.org/project/drupal/-/merge_requests/8198#note_317834
  2. Consider if improvements introduced by #3473374: Improve Dynamic Page Cache header assertions in JSON:API tests (originally invented in this issue) should be merged first or can be merged as part of this issue.

User interface changes

None

API changes

None

Data model changes

None

Release notes snippet

N/A

Issue fork drupal-3278759

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

danflanagan8 created an issue. See original summary.

danflanagan8’s picture

Issue summary: View changes
danflanagan8’s picture

danflanagan8’s picture

Here's one with Media that looks really, really similar:

danflanagan8’s picture

Ok, I think I have found "the" related issue: #2809177: Introduce entity permission providers

Particularly with regard to this comment: #2809177-60: Introduce entity permission providers

I'll leave this open for now, but probably it should be closed as a duplicate of that one (though not an obvious duplicate) or at least postponed on that one.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

acbramley’s picture

Component: cache system » node system
Status: Active » Needs review
StatusFileSize
new1.07 KB

I think the fix will have to look something like this, although since we are returning early that may break other things but I don't really see any way around it. Side note - these "own" permissions a pretty problematic wrt. caching!

Obviously needs tests and probably breaks some existing stuff, perhaps we just detect the presence of the "own" permission and always add the user cache context, then keep the rest as is.

We can also remove the cachePerPermissions since cachePerUser is higher in the hierarchy.

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative, +Needs tests

This issue is being reviewed by the kind folks in Slack, #needs-review-queue-initiative. We are working to keep the size of Needs Review queue [2700+ issues] to around 400 (1 month or less), following Review a patch or merge request as a guide.

As a bug it will need a test case.

Did not review.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

mxr576’s picture

-    if ($operation === 'view' && !$status && $account->hasPermission('view own unpublished content') && $account->isAuthenticated() && $account->id() == $uid) {
-      return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($node);
+    if ($operation === 'view' && !$status && $account->hasPermission('view own unpublished content') && $account->isAuthenticated()) {
+      return AccessResult::allowedIf($account->id() == $uid)->cachePerUser()->addCacheableDependency($node);
     }
 

Did some testing today and unfortunately this is not that simple. By removing $account->id() == $uid we eliminate the chance for granting access to an unpublished node created by somebody else. (Got to this conclusion when I worked on the fix for #3449181: The Content overview page filters out unpublished nodes when a node access module is enabled)

So maybe the only way is what @wim.leers suggested in #2982770-5: NodeAccessControlHandler::checkAccess() does not add a necessary cache context.

mxr576’s picture

StatusFileSize
new19.52 KB

I have spent several hours trying to reproduce this issue using only the available testing tools, aiming for a more minimalist approach than described here.

I attempted to follow the reproduction steps from #2982770-5: NodeAccessControlHandler::checkAccess() does not add a necessary cache context, but I discovered significant differences between how Views and JSONAPI handle entity access checks. JSONAPI performs entity access checks, including the "view own unpublished content" check, whereas Views relies on hook_node_grants() via query alteration out of the box. (See my failed attempts in the attached unable_to_reproduce.patch).

As a result, I had to create a custom test controller, which I will include in my next merge request (MR).

mxr576’s picture

Issue summary: View changes
mxr576’s picture

Closed #2982770: NodeAccessControlHandler::checkAccess() does not add a necessary cache context as duplicate of this one. Also transferred the latest proposed fix from there.

Issue credits should be transferred.

mxr576’s picture

Issue tags: -Needs tests
mxr576’s picture

Hide patch #8 based on #11.

mxr576’s picture

Issue summary: View changes

Updated the issue summary

mxr576’s picture

Issue summary: View changes
mxr576’s picture

Fixing this issue probably also resolves this one.

mxr576’s picture

Issue summary: View changes
Status: Needs work » Needs review

hm, this should have been in "needs review" for a while :)

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new90 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

mxr576’s picture

Status: Needs work » Needs review
mxr576’s picture

Issue summary: View changes
mxr576’s picture

bbrala’s picture

Status: Needs review » Needs work

Did a review. Caching is hard and hurts my head. Left some comments for clarification and some small changes.

In general this makes a lot of sense and a i see no glaring issues.

bbrala’s picture

Title: Access cacheability is not correct when "view own unpublished content" is in use » [PP-1] Access cacheability is not correct when "view own unpublished content" is in use
Status: Needs work » Postponed

Ok, i reviewed this and would RTBC. Although caching is scary we have pretty good proof on the effects. Code looks good, the ->id() issue is fine like this.

Dont think we need a CR here.

Unfortunatly this is postponed on #2973356: Cacheability information from route access checker access results are ignored by dynamic_page_cache so we need to postpone on that.

mxr576’s picture

Thanks for the review and the tentative RTBC. @bbrala.

I have synced with @kristiaanvandeneynde on Slack and described the current plan to get the blocker in sooner than later :) See: #2973356-32: Cacheability information from route access checker access results are ignored by dynamic_page_cache :fingers-crossed:

quietone’s picture

Issue tags: +Bug Smash Initiative

This was a bugsmash target today. This looks well in hand. I have added the 'blocker' tag to #2973356: Cacheability information from route access checker access results are ignored by dynamic_page_cache because of comment #31. It is good to see the the issue this is postponed on is in the remaining tasks.

mxr576’s picture

mxr576’s picture

mxr576’s picture

Title: [PP-1] Access cacheability is not correct when "view own unpublished content" is in use » Access cacheability is not correct when "view own unpublished content" is in use
Assigned: Unassigned » mxr576

No blockers anymore! \o/ just need to rebase...

mxr576’s picture

Assigned: mxr576 » Unassigned
mxr576’s picture

Status: Postponed » Needs review
mxr576’s picture

Opened #3473374: Improve Dynamic Page Cache header assertions in JSON:API tests for the improvements that I made on \Drupal\Tests\jsonapi\Functional\ResourceTestBase1 to ensure the test suite is less fragile and more intuitive to use in context of Dynamic Page Cache assertions.

I am going to reorganize existing changes in MR#8198 to separate changes that belongs to here from those that belongs to 3473374.

mxr576’s picture

This should be it... now we have 3 meaningful commits in the branch.
https://git.drupalcode.org/project/drupal/-/merge_requests/8198/diffs?di...

I may saw some other areas in ResourceTestBase still with hardcoded Dynamic Cache Header values... checking those.

mxr576’s picture

Issue summary: View changes
kristiaanvandeneynde’s picture

Status: Needs review » Needs work

Cacheable metadata, most notably cache contexts, is not being carried over to the next checks in the access control handler. This can lead to cache misses or worst case information disclosure.

mxr576’s picture

Status: Needs work » Needs review
kristiaanvandeneynde’s picture

Status: Needs review » Needs work

Until #3452631: Introduce AccessResult::andAllowedIf(), orAllowedIf(), andForbiddenIf() and orForbiddenIf() to eventually replace orIf() and andIf() lands, I wouldn't use andIf() like you do. It means we are running code when we could have already exited early.

Gradually build a $cacheable_metadata object and add that to $result whenever you want to return it. That still allows for early returns, but does not lose important cacheable metadata.

E.g.:

$cacheable_metadata->addCacheContexts(['foo']);
if (is_foo()) {
  return AccessResult::allowed()->addCacheableDependency($cacheable_metadata);
}

$cacheable_metadata->addCacheContexts(['bar']);
if (is_bar()) {
  return AccessResult::allowed()->addCacheableDependency($cacheable_metadata);
}

// etc.
mxr576’s picture

Do you have concrete suggestions for code adjustments? I know those new methods will bring improvements but with the current tooling I tried hard in the latest commits to return as early as possible, but also keeping collected cacheability instead of ignoring them. But the chance is there that something can be still further improved

mxr576’s picture

user.roles:authenticated context bugs me since the latest change... maybe I can delay that and first check instead whether the user has the given permission, if yes, I can double check whether it is anonymous or not and only add that context, otherwise just vary per permissions.

mxr576’s picture

Status: Needs work » Needs review
mxr576’s picture

kristiaanvandeneynde’s picture

I think I noticed something off about the MR, will comment in detail during contrib time at DrupalCon

kristiaanvandeneynde’s picture

Status: Needs review » Needs work

The cache per user needs to be up higher where I left the remark. Changes are looking really good otherwise! Will do a full review once you've updated it.

mxr576’s picture

The issue here is that we're dealing with a passed in account rather than the current user so any user.foo cache context is technically not correct here.

YES...YES...YES!!! And I always feel bad when I have to bubble up cache per current user in anyways in an entity access handler method just to ensure things "do not get broken" but semantically that is completely incorrect. That said, this is what we have currently... :(

Thanks for the review, to be honest, I have lost a bit now... :) Could you please commit the expected change to the MR?

And before you do that, let's remember that we try to avoid cache per user as long as we can, this is the reason why user.roles:antonymous or user.roles:authenticated (in the current version) s the primary driver to differentiate access for logged in and non-logged in users instead of returning something that varies per user. We only do this as long as we can, so when a node_access module enabled than we have add user.node_grants: that basically makes every result vary by user in a different way.

kristiaanvandeneynde’s picture

Thanks for the review, to be honest, I have lost a bit now... :) Could you please commit the expected change to the MR?

It's late so could have made a mistake, but I stayed closer to the original code while making sure cacheable metadata is passed down after every check. Please review.

mxr576’s picture

Assigned: Unassigned » mxr576

Did a check on the changes and I generally agree with those. I hope nobody is going to raise scope creep just because for consistency reasons not on the view operation was touched.

There are some tests that needs to be fixed, I am going to working on that.

mxr576’s picture

Assigned: mxr576 » Unassigned
mxr576’s picture

Status: Needs work » Reviewed & tested by the community

I take the opportunity that after so many back and forth code reviews with Kristiaan, mark this as RTBC. My changes since his last changes mainly impacted failing tests after his changes, that were changed by me previously.

mxr576’s picture

Rebased and brought in latest changes from [##3473374] which also had to be rebased, but we found other regressions in 11.x in that: https://git.drupalcode.org/project/drupal/-/merge_requests/9465#note_386330

larowlan’s picture

Status: Reviewed & tested by the community » Needs work

Left some comments on the MR

I'm wondering if the cure is worse than the symptom here

mxr576’s picture

Thanks for the review, let me start with a the rebase since there is work to be done it seems.

❯ git rebase origin/11.x
Successfully rebased and updated refs/heads/3278759-access-cacheability.
mxr576’s picture

Status: Needs work » Needs review

Thanks for the review @larowlan!

larowlan’s picture

I'll discuss this with other core committers

larowlan’s picture

I wonder if we can expand the description of the permission to convey that there is a performance impact of granting this permission. And whether we should also have a hook_requirements that shows a warning if this permission has been granted. Will report back on other committers' thoughts.

berdir’s picture

Not fully caught up with the implementation, but there are plenty of valid cases to use own permissions, it's only wrong when granted unnecessarily. But the situation around unpublished view and edit access is messy, we can't really point to a different permission that might not actually exist depending on the modules you use. So I don't think we can remove or warn about its usage.

mxr576’s picture

So I don't think we can remove or warn about its usage.

+1

For changing the world, please join this other issue to brainstorm on how to make node grants and entity access work better together (less confusing to use): https://www.drupal.org/project/drupal/issues/3452449

kristiaanvandeneynde’s picture

I'm wondering if the cure is worse than the symptom here

The symptom is a potentially unstable or even insecure website, so the cure is definitely not worse :)

smustgrave’s picture

Status: Needs review » Needs work

Appears to have some open threads.

mxr576’s picture

Status: Needs work » Needs review

Thanks for the additional feedbacks, I have pushed back on some with answers and accepted one. Back to review

kristiaanvandeneynde’s picture

Okay so I saw @bbrala's comment about isNew() now, but I'd still say we should change the $node->id() boolean checks to a version of isNew(). Reason being that if isNew() ever gets updated to be more strict, we immediately benefit from that change. If we keep manual ID to boolean conversions, then we may end up lagging behind on core changes.

MR looks good to me now for the parts that I can review confidently (so anything but jsonapi tests). Don't let my isNew() comment hold you back, although I think it'd be a nice change.

Test fails seem to be random ones, so if you can ping @bbrala or @larowlan for a final approval then it's RTBC in my book.

mxr576’s picture

Yes they were random failures, restart make them disappear.

Test fails seem to be random ones, so if you can ping @bbrala or @larowlan for a final approval then it's RTBC in my book.

+1

bbrala’s picture

Status: Needs review » Needs work

Really only see the isnew issue. The response to larowlans comment seems valid, and personally I agree with mrx on that. I like to use that method so that when we improve the typing there we get that for free here.

If we can update that I'll rtbc again.

mxr576’s picture

isNew() can be tricked, it does not just check the value of the id, it also most likely does not help PHPStan to narrow down the actual type of $node->id(), but I agree, let's follow the Drupal Way here -- funny, I wanted to use that originally, but I ditched the idea for some reasons, I hope not because tests started to fail :)

  public function isNew() {
    return !empty($this->enforceIsNew) || !$this->id();
  }
mxr576’s picture

Status: Needs work » Needs review
kristiaanvandeneynde’s picture

Status: Needs review » Reviewed & tested by the community

Only open thread is about cache hit ratio and I agree with @mxr576 that security trumps that. We can open a follow-up to further discuss those ramifications if need be.

All aboard the RTBC train, choo choo!

mxr576’s picture

Now that #3473374: Improve Dynamic Page Cache header assertions in JSON:API tests is in 11.x, this needs a rebase because the MR does not apply anymore, because it had a cherry picked version. Maybe I just drop the cherry picked commit and see what happens...

mxr576’s picture

❯ git rebase origin/11.x
Auto-merging core/.phpstan-baseline.php
Auto-merging core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php
CONFLICT (content): Merge conflict in core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php
error: could not apply 07987b49a5... Cherry-picked "Improve Dynamic Page Cache header assertions in JSON:API tests"

I am going to use git merge origin/11.x instead of rebase in a hope that we do not need to restart the review process here.

mxr576’s picture

StatusFileSize
new413.02 KB

Looks good so far, we only have those changes in the MR that we should be, no changes on ResourceTestBase that was fixed in the spin-off issue.

mxr576’s picture

Rebased again, due to random test failure fixed in upstream: https://www.drupal.org/project/drupal/issues/3478621#comment-15875919

mxr576’s picture

Green again! Kept it on RTBC...

kristiaanvandeneynde’s picture

Thanks for updating the MR. Can't immediately see any changes outside of the resolved conflict, so fine with keeping RTBC.

larowlan’s picture

Updating issue credits

  • larowlan committed b346afa1 on 11.1.x
    Issue #3278759 by mxr576, kristiaanvandeneynde, acbramley, danflanagan8...

  • larowlan committed a3127db6 on 11.x
    Issue #3278759 by mxr576, kristiaanvandeneynde, acbramley, danflanagan8...
larowlan’s picture

Version: 11.x-dev » 11.0.x-dev
Status: Reviewed & tested by the community » Patch (to be ported)

Committed to 11.x and backported to 11.1.x

This doesn't apply to 11.0.x/10.5.x or 10.4.x so needs reroll.

Flagging as patch (to be ported) on that basis.

Thanks everyone for the work here

bbrala’s picture

I tried to create a new 11.0.x version of this patch, but there is something a little messy in there, perhaps something else did not get a backport, im not too sure. I'm not sharp enough right now to find out.

mxr576’s picture

It seems #3473374: Improve Dynamic Page Cache header assertions in JSON:API tests has not been backported to 11.0.x yet, so that should be backported first.

mxr576’s picture

10.4.x backport is ready in the related MR.

  • larowlan committed aa747ca7 on 10.4.x
    Issue #3278759 by mxr576, kristiaanvandeneynde, acbramley, danflanagan8...

  • larowlan committed 7102b46b on 10.5.x
    Issue #3278759 by mxr576, kristiaanvandeneynde, acbramley, danflanagan8...
larowlan’s picture

Version: 11.0.x-dev » 10.4.x-dev
Status: Patch (to be ported) » Fixed

Backported to 10.5.x and 10.4.x

A backport to 10.3.x or 11.0.x would also require a backport of #3473374: Improve Dynamic Page Cache header assertions in JSON:API tests which as a task isn't eligible for backport, so marking this as fixed.

Thanks all, especially @mxr576 for sticking with this one.

mxr576’s picture

Thanks all, especially @mxr576 for sticking with this one.

:beers:

A backport to 10.3.x or 11.0.x would also require a backport of #3473374: Improve Dynamic Page Cache header assertions in JSON:API tests which as a task isn't eligible for backport, so marking this as fixed.

Does this highlight an edge case in the backporting policy? Here’s the dilemma:
This fix cannot be backported to 11.0.x due to its dependency on a non-backportable issue. As a result, the upgrade path from 10.4.x/10.5.x to 11.0.x could lead to unexpected and unforeseeable feature or bugfix losses unless projects skip directly to 11.1.x.

And #3473374 was only born to keep this issue clean from test framework fixes and speed up the code review.

larowlan’s picture

Status: Fixed » Patch (to be ported)

Discussed with @catch and he agreed it made sense to backport #3473374: Improve Dynamic Page Cache header assertions in JSON:API tests

larowlan’s picture

Version: 10.4.x-dev » 11.0.x-dev

  • catch committed 7479a46b on 10.5.x
    Revert "Issue #3278759 by mxr576, kristiaanvandeneynde, acbramley,...

  • catch committed d7f67cf0 on 10.4.x
    Revert "Issue #3278759 by mxr576, kristiaanvandeneynde, acbramley,...
catch’s picture

This broke HEAD on 10.5 and 10.4.x, see https://git.drupalcode.org/project/drupal/-/jobs/3585472. Reverted for now.

adamps’s picture

I believe this has caused a regression / BC break: #3495703: Loss of access to unpublished nodes.

acbramley’s picture

Status: Patch (to be ported) » Needs work

Should be NW now since it was reverted?

ericgsmith changed the visibility of the branch 3278759-10.4.x to active.

ericgsmith changed the visibility of the branch 3278759-10.4.x to hidden.

ericgsmith’s picture

Have tried to see where this is at.

The comment in #99 links to the 10.5.x pipeline which we can see is broken - as far as I can tell the 10.4.x pipeline from the same commit is https://git.drupalcode.org/project/drupal/-/pipelines/358805 which did fail but looks like an unrelated failure.

I've reapplied the commit from 11.x to the 10.4.x and it applies cleanly and appears to still pass https://git.drupalcode.org/project/drupal/-/merge_requests/12272/pipelines

I've done the same for 10.5.x here and its failing with what was reported before it was reverted https://git.drupalcode.org/issue/drupal-3278759/-/jobs/5409866#L412

The reason appears to be what was outlined here in #91 - except that the related issue #3473374: Improve Dynamic Page Cache header assertions in JSON:API tests was committed to 10.4.x but does not appear to be on 10.5.x. That issue was closed as fixed - but my assumption here (have not tested) is that if that issue is backported to 10.5.x then this should pass.

I'm a bit confused on the timeline of when branches were open - but should this be postposed on #3473374: Improve Dynamic Page Cache header assertions in JSON:API tests - and should #3473374: Improve Dynamic Page Cache header assertions in JSON:API tests be reopened so that it can be applied to 10.5.x? Seems very unexpected that we have something in 10.4.x but not in 10.5.x?

Edit - I've reopened that issue and added a MR for 10.5.x

rosk0’s picture

Version: 11.0.x-dev » 10.4.x-dev
Status: Needs work » Reviewed & tested by the community

Reviewed merge request #12272 - diff is identical to the 11.x commit, pipeline is green, so I believe this is RTBC. Setting version accordingly.

I believe this should also be ported to the 10.5.x. and 10.6.x once #3473374: Improve Dynamic Page Cache header assertions in JSON:API tests is committed on those branches.

longwave made their first commit to this issue’s fork.

longwave’s picture

So currently this is in all supported 11.x versions but not 10.x.

#3473374: Improve Dynamic Page Cache header assertions in JSON:API tests landed in 10.6.x and 10.5.x finally, I've rebased MR !12273 against 10.5.x and if this is green now then this means this can be committed to both those branches.

I'm also closing the 11.0.x and 10.4.x MRs as they are no longer eligible for backports.

  • longwave committed 53f117df on 10.5.x
    Issue #3278759 by mxr576, kristiaanvandeneynde, ericgsmith, acbramley,...

  • longwave committed 86faa69f on 10.6.x
    Issue #3278759 by mxr576, kristiaanvandeneynde, ericgsmith, acbramley,...
longwave’s picture

Status: Reviewed & tested by the community » Fixed

!12273 is green now so this should mean no test failures once this is committed to 10.5.x and 10.6.x. Thanks for the patience and assistance with the backports here.

Committed and pushed 86faa69f084 to 10.6.x and 53f117df5f0 to 10.5.x. Thanks!

Status: Fixed » Closed (fixed)

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