Problem/Motivation
Dynamic page cache currently ignores cacheability information from route access results which could lead to unexpected edge cases. Imagine a page where the access is decided per user bases but the content does not vary per user. Without adding user context to the render array/response of the page, the dynamic page cache module is not going to cache it per user.
Having this fixed is currently a blocker of another issue and the original fix for this problem were invented there: #3278759: Access cacheability is not correct when "view own unpublished content" is in use See also: https://git.drupalcode.org/project/drupal/-/merge_requests/8198#note_317834
Original description
Discovered in #2869426: EntityResource should add _entity_access requirement to REST routes.
See #2869426-28: EntityResource should add _entity_access requirement to REST routes, #2869426-29: EntityResource should add _entity_access requirement to REST routes and #2869426-83: EntityResource should add _entity_access requirement to REST routes in particular.
#29:
I could be wrong, but I remember that we once discussed that it is by design that route access cacheability is *not* considered by dynamic page cache because dynamic page cache runs *after* route access checking.
But maybe I misunderstood something here.
To my surprise 😲 and fear 😱, this was NEVER DISCUSSED! Zero mentions of
route access. #2429617-219: Make D8 2x as fast: Dynamic Page Cache: context-dependent page caching (for *all* users!) mentions it. But doesn't dive in deeper. So I instead looked at every occurrence ofaccess. Again nothing mentioned.But I think @Berdir is right. It's just unfortunate that despite Dynamic Page Cache having such detailed test coverage, this is not something that is explicitly tested. (If it were, then the change made to
\Drupal\Core\EventSubscriber\RouteAccessResponseSubscriber::getSubscribedEvents()should trigger a test failure.)And I think the reason this is only coming up now and not months or years ago, is that the REST module's responses are atypical. Unlike pretty much all other responses, the controller in question (
\Drupal\rest\Plugin\rest\resource\EntityResource::get()) has been:
- Doing its own route/response-level access checking.
- Significantly varying the contents of the response based on that access checking.
(The closest analogies are block and entity rendering, but those are not route/response-level, they're intra-response-level.)
Proposed resolution
TBD
Remaining tasks
- Fix #3452426: Insufficient cacheability information bubbled up by UserAccessControlHandler that was identified in comment 26 by kristiaanvandeneynde OCD/spidy-sense
- Get the #3452181: VariationCache needs to be more defensive about cache context manipulation to avoid broken redirects so it can spot potential issues revealed by this fix
- Fix #3454346: JsonApiRequestValidator does not set cacheable metadata when the filter allows the request that was discovered in the VariationCache hardening issue
- Fix #2719721: BreadcrumbBuilder::applies() mismatch with cacheability metadata that was discovered in the VariationCache hardening issue
User interface changes
None.
API changes
None.
Data model changes
Yes: Dynamic Page Cache would vary not just by the Response's cacheability, but also by the route access result's cacheability.
| Comment | File | Size | Author |
|---|
Issue fork drupal-2973356
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:
- 2973356-route-access-check-cacheability-ignored
changes, plain diff MR !8221
Comments
Comment #2
wim leersComment #3
berdirI don't remember where, but I'm ~90% sure that we at some discussed the route access thing, I think because I investigated some missing cacheablity on a response and was surprised to not see it because it was there on the final response. And it was because it was only added by route access. And you told me that is by design because of some reason. No idea where it was but I think I do remember the reason.
DPC caches based on the *route*, so we need to have that. And route matching and access happens at the same time (AccessAwareRouter) for security reasons. Which means even if DPC wanted to run before checking route access, it couldn't. Which also means that it would be a waste to consider route access cacheablity as that would just introduce the possibility of more variations that are not actually relevant (or should not be).
Comment #4
wim leersThat sounds super sensible! In that case, all is well, and we just need to expand DPC's test coverage to explicitly test this.
By the way, thank you for responding so quickly and so thoroughly! 😮 I didn't ping you at all, so you must be monitoring your d.o notification e-mails like a hawk!
Comment #15
mxr576and I think I have just run into this here: https://git.drupalcode.org/project/drupal/-/merge_requests/8198#note_317834
Comment #17
mxr576Comment #18
mxr576Comment #19
kristiaanvandeneyndeWhere is this query coming from? Seems unrelated:
'SELECT "name", "value" FROM "key_value" WHERE "name" IN ( "theme:stark" ) AND "collection" = "config.entity.key_store.block"'Comment #20
berdirThat's the entity query index lookup for the blocks of the current theme, i guess means a dynamic page cache miss that previously was a hit?
Comment #21
mxr576Probably, tbh, I have no clue on that.
Comment #22
kristiaanvandeneyndeYeah that's what has me a tiny bit worried. The StandardPerformanceTest is supposed to mimic a "normal" page visit on a "regular" Drupal site and gauge its performance. If we get an extra query there, it's highly likely we get said extra query everywhere. If it's required for this bug to go away, sure, but this is something we should double-check. We don't want to regress if we can avoid it.
I'll see if I can spare a bit of time to have a look.
Comment #23
mxr576Comment #24
kristiaanvandeneyndeOkay I just did some digging and this extra query makes sense to me now.
So because of the StandardPerformanceTest checking for Login, we are seeing a route that used to have a DPC HIT now be UNCACHEABLE. This route being the user page, which @mxr576 correctly pointed out is where you land after login and therefore, with the fix here, triggering a DPC UNCACHEABLE where it used not to.
Now all of the pieces of the /user/[id] page that could be cached, still are. So the only reasonable thing we can expect to see more queries of is that which is required to build the final response again before it is shipped off to the response subscribers and then the browser.
Chief among these page building elements that has to run again because there was no DPC cache hit is BlockPageVariant, which calls BlockRepository::getVisibleBlocksPerRegion(), which in turn fires the extra query we're seeing.
So on that front: All good, this was to be expected and is a side-effect we can't do anything about here.
The only thing that has me really worried now is that there's apparently nothing on the user page that also sets the user cache context. Because, if there were, this page would (and should) always have been UNCACHEABLE for the DPC.Never mind, forgot that anyone can view /user/[id] given the right permissions and that said page doesn't vary by user that way.Comment #25
kristiaanvandeneyndeGave the whole MR a decent look and would RTBC if there was an answer to what is going on in those json_api tests. I'm not getting into the onRespond priority as I feel as if I'm not qualified to make a judgement there. I'd have to check all other response subscribers first to confidently sign off on that one.
Comment #26
kristiaanvandeneyndeOkay so this issue made me go down the rabbit hole a bit as I was wondering why DPC would mark the user page as UNCACHEABLE even though sometimes it actually is cacheable. Then I found a very interesting bug which will occur after this MR goes in.
Check the following code in UserAccessControlHandler::checkAccess():
There is a bug here where a user without the 'access user profiles' would be locked out of their own profile. This is because anything after the
$account->id() == $entity->id()has to vary by user (and user.permissions, but that gets optimized away), yet the return value at the bottom only varies by user.permissions.So if a user without the permission and without looking at their own profile comes along, we return a neutral access result for said profile varying by user.permissions only. Meaning if the user whom the profile belongs to comes along next, and happens to have the same permissions as the previous visitor, they would get the cached response with AccessResultNeutral!
This currently does not manifest itself because DPC doesn't take the access checks' cacheability into account, but given how user.permissions does not lead to an unceacheable response, DPC could very well be caching an access denied page and serve it to the user who should get access. At least, once this MR gets in.
Not setting back to NW because I don't necessarily think this is the place to fix said bug, but it will occur once this MR gets accepted.
The fix would probably look like this:
Comment #27
mxr576Thanks @kristiaanvandeneynde for the detailed testing and reporting, another rabbit hole indeed.
The #3278759: Access cacheability is not correct when "view own unpublished content" is in use already has a couple of blockers, another child issue of a child issue would not hurt much ;S
Would you like to open a new issue for this or check if there is an existing one? (You deserve a (double) credit for this finding)
Comment #28
kristiaanvandeneyndeI'll try to open one next week and attach the current code in the MR. Catch requested we use code more similar to ContactPageAccess, but now I'm thinking that class is bugged too. We can update the MR once I'm done researching that (or someone else can jump in and research it too).
Shall we postpone this issue or somehow mark that it can't be committed in its current state lest we introduce bugs and potentially even security issues?
Comment #29
mxr576Added a note about this to the issue description, but I hope this does not become a chicken-or-egg story, since for me, this is already a spin-off/prerequisite of fixing #3278759: Access cacheability is not correct when "view own unpublished content" is in use.
Comment #30
kristiaanvandeneyndeOpened #3452181: VariationCache needs to be more defensive about cache context manipulation to avoid broken redirects, haven't opened an issue for the UserAccessControlHandler bug yet as I'd like to see how many more surface once the VC MR is ready.
Comment #31
mxr576#2973356-26: Cacheability information from route access checker access results are ignored by dynamic_page_cache now has a dedicated issue: #3452426: Insufficient cacheability information bubbled up by UserAccessControlHandler
Comment #32
mxr576Synched with @kristiaanvandeneynde on Slack about the plan for getting this closed sooner than later, updated the issue description accordingly.
Comment #33
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 #34
mxr576Added void return defs to tests again... PHPStorm could easily resolve that.
Comment #35
andypostComment #36
quietone commentedAccording to this comment, this is blocking that issue.
Comment #37
mxr576The last second level blockers just got closed, let's make a comment and hope that flushes stale render cache ;P
Comment #38
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 #39
mxr576Comment #40
naveenvalechaAdded tag for #39
Comment #41
mxr576@naveenvalecha, review bot was right, Gitlab also complained about the merge issue, that tag should be used wisely.
Comment #42
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 #43
mxr576Rebased to make NR bot happy.
Comment #44
mxr576Let's rebase to see if anything new reports an error before VariationCache improvement gets merged and unblocks this issue...
Comment #45
mxr576kewl! Nothing got broken
Comment #46
mxr576Okay, so unless exit codes are lost somewhere, the upcoming VariationCache improvement won't spot any new issues in Drupal core after this change. Fingers crossed...
https://git.drupalcode.org/issue/drupal-2973356/-/commit/8aef8dd16ce8ec4...
Comment #47
mxr576Rebasing since #3452181: VariationCache needs to be more defensive about cache context manipulation to avoid broken redirects is in 11.x now \o/
Comment #48
mxr576No conflicts. hopefully this gets smooth...
Comment #49
mxr576Created two change records for this issue.
Comment #50
kristiaanvandeneyndeFound 2 nits, everything else looks okay. The only thing that still has me worried a bit is the jsonapi tests becoming uncacheable. It might very well be intentional and therefore harmless, but it would be great if we could git blame, find the original issue and then ask around to make sure why a NULL access result is allowed and added as a dependency (therefore setting max age to 0).
Comment #51
kristiaanvandeneyndeSeems good enough for me. Could you please inline the variable still, though?
Comment #52
mxr576Gitlab complained about a merge conflict so I had to rebase, No manual steps were needed locally.
Comment #53
catchIs there a follow-up issue for
\Drupal\jsonapi\Controller\FileUpload::handleFileUploadForExistingResource()?Comment #54
mxr576I haven't opened it yet, I was consider it, but that issue is two folded, one is the sub-request, but the first blocker is
\Drupal\Core\PageCache\RequestPolicy\CommandLineOrUnsafeMethodthat makes POST requests uncacheable dynamic cache. (IMO does not change anything on this https://www.drupal.org/node/3420901)https://git.drupalcode.org/project/drupal/-/merge_requests/8221#note_368194
So I can register this finding as an issue, but that is going to be blocked by making POST requests cacheable by Dynamic Page Cache, which I do not know yet if it is on the roadmap or not.
Comment #55
catchIt hasn't been discussed for dynamic page cache, but we added read-only render caching for post requests in general, and I think we could probably apply similar to dynamic page cache too - would be worth a try! #3395776: Make POST requests render cacheable.
Comment #56
catchWent ahead and opened #3473158: Read-only render caching on POST requests for dynamic page cache.
Comment #59
catchCommitted/pushed to 11.x and cherry-picked to 10.4.x, thanks!
This would probably be committable to a patch release too if it wasn't for the service priority changes, given those, leaving in the two minor releases.
Comment #62
catchComment #63
mxr576Thanks @catch for the merge and delaying with opening the follow up issue.
Comment #64
moshe weitzman commentedFYI, devel's tests started failing after this merge. Its not clear to me why, since devel uses lazy builders. https://gitlab.com/drupalspoons/devel/-/issues/541.