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
- 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
- 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
| Comment | File | Size | Author |
|---|---|---|---|
| #74 | Screenshot_20241127_101337.png | 413.02 KB | mxr576 |
Issue fork drupal-3278759
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
Comment #2
danflanagan8Comment #3
danflanagan8This issue is certainly related, but I don't think it's the same thing.
Comment #4
danflanagan8Here's one with Media that looks really, really similar:
Comment #5
danflanagan8Ok, 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.
Comment #8
acbramley commentedI 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.
Comment #9
smustgrave commentedThis 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.
Comment #11
mxr576Did some testing today and unfortunately this is not that simple. By removing
$account->id() == $uidwe 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.
Comment #12
mxr576I 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).
Comment #14
mxr576Comment #15
mxr576Closed #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.
Comment #16
mxr576Comment #17
mxr576Hide patch #8 based on #11.
Comment #18
mxr576Fixing #2973356: Cacheability information from route access checker access results are ignored by dynamic_page_cache first is a blocker of this issue.
Comment #19
mxr576Updated the issue summary
Comment #20
mxr576Comment #21
mxr576Fixing this issue probably also resolves this one.
Comment #22
mxr576hm, this should have been in "needs review" for a while :)
Comment #25
needs-review-queue-bot commentedThe 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.
Comment #26
mxr576Comment #27
mxr576Comment #28
mxr576Comment #29
bbralaDid 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.
Comment #30
bbralaOk, 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.
Comment #31
mxr576Thanks 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:
Comment #32
quietone commentedThis 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.
Comment #33
mxr576Rebased and also cherry-picked the latest MR state from #2973356: Cacheability information from route access checker access results are ignored by dynamic_page_cache.
Comment #34
mxr576Added the latest version of https://git.drupalcode.org/project/drupal/-/merge_requests/8221 to the top of the commit change.
https://git.drupalcode.org/project/drupal/-/merge_requests/8198/diffs?di...
Comment #35
mxr576No blockers anymore! \o/ just need to rebase...
Comment #36
mxr576Comment #37
mxr576Comment #38
mxr576Opened #3473374: Improve Dynamic Page Cache header assertions in JSON:API tests for the improvements that I made on
\Drupal\Tests\jsonapi\Functional\ResourceTestBase1to 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.
Comment #39
mxr576This 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.
Comment #40
mxr576Yep, those three could be still automated. https://git.drupalcode.org/project/drupal/-/merge_requests/8198/diffs?di...
Comment #41
kristiaanvandeneyndeCacheable 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.
Comment #42
mxr576Comment #43
kristiaanvandeneyndeUntil #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.:
Comment #44
mxr576Do 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
Comment #45
mxr576user.roles:authenticatedcontext 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.Comment #46
mxr576Comment #47
mxr576#3473374: Improve Dynamic Page Cache header assertions in JSON:API tests got an RTBC \o/
Comment #48
kristiaanvandeneyndeI think I noticed something off about the MR, will comment in detail during contrib time at DrupalCon
Comment #49
kristiaanvandeneyndeThe 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.
Comment #50
mxr576YES...YES...YES!!! And I always feel bad when I have to bubble up
cache per current userin 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:antonymousoruser.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 adduser.node_grants:that basically makes every result vary by user in a different way.Comment #51
kristiaanvandeneyndeIt'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.
Comment #52
mxr576Did 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.
Comment #53
mxr576My changes after Kristiaan's changes: https://git.drupalcode.org/project/drupal/-/merge_requests/8198/diffs?st...
Comment #54
mxr576I 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.
Comment #55
mxr576Rebased 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
Comment #56
larowlanLeft some comments on the MR
I'm wondering if the cure is worse than the symptom here
Comment #57
mxr576Thanks for the review, let me start with a the rebase since there is work to be done it seems.
Comment #58
mxr576Thanks for the review @larowlan!
Comment #59
larowlanI'll discuss this with other core committers
Comment #60
larowlanI 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.
Comment #61
berdirNot 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.
Comment #62
mxr576+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
Comment #63
kristiaanvandeneyndeThe symptom is a potentially unstable or even insecure website, so the cure is definitely not worse :)
Comment #64
smustgrave commentedAppears to have some open threads.
Comment #65
mxr576Thanks for the additional feedbacks, I have pushed back on some with answers and accepted one. Back to review
Comment #66
kristiaanvandeneyndeOkay 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.
Comment #67
mxr576Yes they were random failures, restart make them disappear.
+1
Comment #68
bbralaReally 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.
Comment #69
mxr576isNew() 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 :)Comment #70
mxr576Comment #71
kristiaanvandeneyndeOnly 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!
Comment #72
mxr576Now 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...
Comment #73
mxr576I am going to use
git merge origin/11.xinstead of rebase in a hope that we do not need to restart the review process here.Comment #74
mxr576Looks 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.
Comment #75
mxr576Rebased again, due to random test failure fixed in upstream: https://www.drupal.org/project/drupal/issues/3478621#comment-15875919
Comment #76
mxr576Green again! Kept it on RTBC...
Comment #77
kristiaanvandeneyndeThanks for updating the MR. Can't immediately see any changes outside of the resolved conflict, so fine with keeping RTBC.
Comment #78
larowlanUpdating issue credits
Comment #82
larowlanCommitted 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
Comment #83
bbralaI 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.
Comment #84
mxr576It 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.
Comment #86
mxr57610.4.x backport is ready in the related MR.
Comment #89
larowlanBackported 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.
Comment #91
mxr576:beers:
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.
Comment #92
larowlanDiscussed with @catch and he agreed it made sense to backport #3473374: Improve Dynamic Page Cache header assertions in JSON:API tests
Comment #93
larowlanComment #99
catchThis broke HEAD on 10.5 and 10.4.x, see https://git.drupalcode.org/project/drupal/-/jobs/3585472. Reverted for now.
Comment #100
adamps commentedI believe this has caused a regression / BC break: #3495703: Loss of access to unpublished nodes.
Comment #101
acbramley commentedShould be NW now since it was reverted?
Comment #106
ericgsmith commentedHave 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
Comment #107
rosk0Reviewed 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.
Comment #109
longwaveSo 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.
Comment #114
longwave!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!