Problem/Motivation

It's easy to forget the user cache context when doing stuff based on the current user.

Proposed resolution

Add the cache context and tag automatically whenever any user-specific data is accessed on the current_user service.

Remaining tasks

None.

User interface changes

None.

API changes

None.

Data model changes

None.

CommentFileSizeAuthor
#84 interdiff-75-84.txt7.13 KBeffulgentsia
#84 accountproxy_cacheability_bubbled_automatically-2558599-84.patch18.32 KBeffulgentsia
#80 interdiff.txt7.35 KBwim leers
#80 accountproxy_cacheability_bubbled_automatically-2558599-80.patch21.1 KBwim leers
#78 interdiff.txt3.98 KBwim leers
#78 accountproxy_cacheability_bubbled_automatically-2558599-78.patch21.19 KBwim leers
#77 interdiff.txt748 byteswim leers
#77 accountproxy_cacheability_bubbled_automatically-2558599-77.patch24.15 KBwim leers
#75 interdiff.txt4.7 KBeffulgentsia
#75 accountproxy_cacheability_bubbled_automatically-2558599-75.patch23.81 KBeffulgentsia
#73 interdiff.txt2.15 KBeffulgentsia
#73 accountproxy_cacheability_bubbled_automatically-2558599-73.patch22.36 KBeffulgentsia
#70 interdiff.txt2.63 KBeffulgentsia
#70 accountproxy_cacheability_bubbled_automatically-2558599-70.patch20.09 KBeffulgentsia
#69 interdiff.txt5.16 KBeffulgentsia
#69 accountproxy_cacheability_bubbled_automatically-2558599-69.patch20.45 KBeffulgentsia
#68 interdiff.txt1.31 KBeffulgentsia
#68 accountproxy_cacheability_bubbled_automatically-2558599-68.patch19.11 KBeffulgentsia
#66 interdiff.txt1.69 KBeffulgentsia
#66 accountproxy_cacheability_bubbled_automatically-2558599-66.patch18.45 KBeffulgentsia
#65 interdiff.txt1.11 KBeffulgentsia
#65 accountproxy_cacheability_bubbled_automatically-2558599-65.patch20.06 KBeffulgentsia
#63 interdiff.txt1.72 KBeffulgentsia
#63 accountproxy_cacheability_bubbled_automatically-2558599-63.patch19.32 KBeffulgentsia
#61 interdiff.txt4.91 KBeffulgentsia
#61 accountproxy_cacheability_bubbled_automatically-2558599-61.patch20.93 KBeffulgentsia
#59 accountproxy_cacheability_bubbled_automatically-2558599-57.patch15.87 KBeffulgentsia
#57 interdiff-18-57.patch3.72 KBeffulgentsia
#57 accountproxy_cacheability_bubbled_automatically-2558599-57.patch15.87 KBeffulgentsia
#53 interdiff.txt24.3 KBwim leers
#53 accountproxy_cacheability_bubbled_automatically-2558599-53.patch24.81 KBwim leers
#52 interdiff.txt1.09 KBwim leers
#52 accountproxy_cacheability_bubbled_automatically-2558599-52.patch45.44 KBwim leers
#45 interdiff.txt4.49 KBwim leers
#45 accountproxy_cacheability_bubbled_automatically-2558599-45.patch44.41 KBwim leers
#44 interdiff.txt1.42 KBwim leers
#44 accountproxy_cacheability_bubbled_automatically-2558599-44.patch40.27 KBwim leers
#42 interdiff.txt910 byteswim leers
#42 accountproxy_cacheability_bubbled_automatically-2558599-42.patch41.64 KBwim leers
#41 interdiff.txt1.49 KBwim leers
#41 accountproxy_cacheability_bubbled_automatically-2558599-41.patch40.82 KBwim leers
#38 interdiff.txt1.65 KBwim leers
#38 accountproxy_cacheability_bubbled_automatically-2558599-38.patch40.53 KBwim leers
#37 interdiff.txt664 byteswim leers
#37 accountproxy_cacheability_bubbled_automatically-2558599-37.patch38.94 KBwim leers
#34 interdiff.txt4.43 KBwim leers
#34 accountproxy_cacheability_bubbled_automatically-2558599-34.patch38.33 KBwim leers
#33 interdiff.txt2.06 KBwim leers
#33 accountproxy_cacheability_bubbled_automatically-2558599-33.patch34.68 KBwim leers
#23 interdiff.txt663 byteswim leers
#23 accountproxy_cacheability_bubbled_automatically-2558599-23.patch32.88 KBwim leers
#22 interdiff.txt3.59 KBwim leers
#22 accountproxy_cacheability_bubbled_automatically-2558599-22.patch32.29 KBwim leers
#21 interdiff.txt1.54 KBwim leers
#21 accountproxy_cacheability_bubbled_automatically-2558599-21.patch29.38 KBwim leers
#20 interdiff.txt19.25 KBwim leers
#20 accountproxy_cacheability_bubbled_automatically-2558599-20.patch28.5 KBwim leers
#18 interdiff.txt7.36 KBwim leers
#18 accountproxy_cacheability_bubbled_automatically-2558599-18.patch12.4 KBwim leers
#17 interdiff.txt926 byteswim leers
#17 accountproxy_cacheability_bubbled_automatically-2558599-17.patch5.63 KBwim leers
#16 interdiff.txt796 byteswim leers
#16 accountproxy_cacheability_bubbled_automatically-2558599-16.patch5.33 KBwim leers
#15 accountproxy_cacheability_bubbled_automatically-2558599-15.patch5.28 KBwim leers
#11 interdiff.txt2.02 KBwim leers
#11 accountproxy_cacheability_bubbled_automatically-2558599-11.patch5.28 KBwim leers
#8 interdiff.txt914 byteswim leers
#8 accountproxy_cacheability_bubbled_automatically-2558599-8.patch5.89 KBwim leers
#6 interdiff.txt3.27 KBwim leers
#6 accountproxy_cacheability_bubbled_automatically-2558599-6.patch5.79 KBwim leers
#2 accountproxy_cacheability_bubbled_automatically-2558599-2.patch5.63 KBwim leers

Issue fork drupal-2558599

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

Wim Leers created an issue. See original summary.

wim leers’s picture

Status: Active » Needs review
Issue tags: +Needs tests
StatusFileSize
new5.63 KB
andypost’s picture

are you sure that this approach will not abuse cache_tags table more?

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new5.79 KB
new3.27 KB

At least some of the failures are because CacheabilityBubblingAccountProxy is missing use DependencySerializationTrait. Which made me realize that this change actually forces everything using the current_user service to initialize the render and theme system (because current_user -> renderer -> theme.manager).

So the proper solution is to not inject the renderer. Otherwise even REST responses may get the theme system initialized.

Status: Needs review » Needs work
wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new5.89 KB
new914 bytes

Stupid me, I'm of course still getting a service injected. So I still need DependencySerializationTrait.

Status: Needs review » Needs work
dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Session/CacheabilityBubblingAccountProxy.php
    @@ -0,0 +1,171 @@
    + * Drupal 8 to cache aggressively yet still
    

    ... stop at any given point at a sentence :)

  2. +++ b/core/lib/Drupal/Core/Session/CacheabilityBubblingAccountProxy.php
    @@ -0,0 +1,171 @@
    +  public function id() {
    +    return $this->accountProxy->id();
    +  }
    ...
    +  public function getUserName() {
    +    $this->bubble();
    +    return $this->accountProxy->getUsername();
    +  }
    

    Can we at least document why we don't bubble on uid?

  3. +++ b/core/modules/user/user.services.yml
    @@ -42,7 +42,7 @@ services:
       theme.negotiator.admin_theme:
         class: Drupal\user\Theme\AdminNegotiator
    -    arguments: ['@current_user', '@config.factory', '@entity.manager', '@router.admin_context']
    +    arguments: ['@current_user.non_bubbling', '@config.factory', '@entity.manager', '@router.admin_context']
         tags:
    

    Why do we need this change. Why do we opt out for that specific usecase?

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new5.28 KB
new2.02 KB
  1. Hah! Fixed.
  2. We should! This is exactly what Fabian suggested in #53 at the other issue, but which I still needed to address. I first wanted to start reducing the number of failures. Done now.
  3. Because otherwise there's a circular dependency. Except that that is no longer the case as of #6!

Status: Needs review » Needs work
berdir’s picture

6 => 'user:0',

For starters, sounds like you want to exclude the anonymous user from this? ;)

effulgentsia’s picture

Issue tags: +Security improvements, +sprint

Tagging same as #2557815-39: Automatically bubble the "user.node_grants:$op" cache context in node_query_node_access_alter(). Similar to that issue, I don't think this one should be promoted to Critical unless real core or contrib security vulnerabilities get discovered during RC that justify us doing so.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new5.28 KB

Now really on this again.

First, a rebased patch.

wim leers’s picture

StatusFileSize
new5.33 KB
new796 bytes

#13: done.

wim leers’s picture

StatusFileSize
new5.63 KB
new926 bytes

Apparently AccountInterface was changed in #2112679: getUsername() should return the username getDisplayName() for the formatted user name; it now has two additional required methods. Updated accordingly.

wim leers’s picture

StatusFileSize
new12.4 KB
new7.36 KB

Fixing test failures caused by excessively added/bubbled cacheability metadata…

Sadly, to achieve that, we need to use the current_user.non_bubbling service in many places. For example, in all cache contexts and in all places where we do access checking.

It seems that as a consequence, we'll end up with a majority of services consuming current_user.non_bubbling instead of current_user. Which means developers looking at core have a good chance of copy/pasting something that will in fact still be insecure…

However, I can reduce the pain there by stopping to inject the current user into many services; many services don't actually need it. Doing that next.

dawehner’s picture

Mh, that is a bit sad, well one alternative one could research is a bit more static code analysis, but that itself is of course orders of magnitude more complex.

wim leers’s picture

StatusFileSize
new28.5 KB
new19.25 KB

Refactored a bunch of services to not pass the current user service to the access manager. That significantly cuts down on the problem described in #18.

wim leers’s picture

StatusFileSize
new29.38 KB
new1.54 KB

Missed one spot.

wim leers’s picture

StatusFileSize
new32.29 KB
new3.59 KB

Fixed the failures in CommentDefaultFormatterCacheTagsTest and ToolbarCacheContextsTest.

AFAICT getTimeZone() should not bubble the user cache context, but the timezone cache context.

wim leers’s picture

StatusFileSize
new32.88 KB
new663 bytes

While working on #22, I noticed that the comment form now has more cache tags (see the changes in CommentDefaultFormatterCacheTagsTest).

This is because filter_formats() (which is used to create the text format dropdown and the filter tips) uses the current user service to personalize it to the list of formats the current user can use. But this list is of course not specific to the user; it depends on the user's permissions. So it actually varies by user.permissions, not user. But, making this work correctly requires modifying filter_formats(), which 1) is a soft BC break at best, an actual BC break at worst, 2) is therefore in any case out of scope here.

So, opening a separate issue for that.

This patch also fixes CommentTranslationUITest's cacheability expectations.

wim leers’s picture

Issue+patch created: #2611924: filter_formats() returns different value based on access, lacks cacheability metadata.

Note that with that patch, we could revert the changes here to CommentDefaultFormatterCacheTagsTest (in #22) and CommentTranslationUITest (in #23), because the cacheability metadata would become more accurate/more refined thanks to that.

Status: Needs review » Needs work

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new34.68 KB
new2.06 KB

One less fail and one less unnecessary @current_user injection.

wim leers’s picture

StatusFileSize
new38.33 KB
new4.43 KB
+++ b/core/core.services.yml
@@ -804,7 +804,7 @@ services:
   router:
     class: Drupal\Core\Routing\AccessAwareRouter
-    arguments: ['@router.no_access_checks', '@access_manager', '@current_user']
+    arguments: ['@router.no_access_checks', '@access_manager', '@current_user.non_bubbling']

I noticed this is actually just like the three contextual link/local action/local task managers: having both the access manager and the current user services injected is pointless, since the access manager already has the current user service injected. So this one can be simplified also, which means again one less occurrence of @current_user.non_bubbling.

Status: Needs review » Needs work
wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new38.94 KB
new664 bytes

Again one less fail.

wim leers’s picture

StatusFileSize
new40.53 KB
new1.65 KB

And another one. Turns out HEAD is injecting \Drupal::currentUser() into \Drupal::currentUser(), LOL.

Status: Needs review » Needs work
wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new40.82 KB
new1.49 KB

The majority of the remaining failures are in REST tests. They're mainly caused by two bits of cacheability metadata being bubbled:

  1. \Drupal\node\Entity\Node::getCurrentUserId(), which is called for the Node entity type's uid base field, to set a default value; the mere act of instantiating a Node object does not mean it will also be rendered, so this should use the non-bubbling service
  2. AccountProxy::setAccount() (this is the class used for the current_user.non_bubbling service) does
    date_default_timezone_set(drupal_get_user_timezone());
    

    and drupal_get_user_timezone() calls \Drupal::currentUser()->getTimezone(). The latter then bubbles the timezone cache context (since #22). But the mere act of ensuring the right timezone for the duration of this request does not mean that any date or time will actually be rendered. In other words: it does not make sense for \Drupal\Core\Session\CacheabilityBubblingAccountProxy::getTimeZone() to do any bubbling at all.

This fixes 5 failures.

P.S.: debugging the second point has unveiled lots of bugs: we seem to call setAccount() twice whenever a user logs in, once with a User entity, once with a UserSession; in getAccount() when $this->account is not set and we default either to the initial account or the anonymous user session, we don't set the timezone; the AnonymousUserSession's timezone property is never set to a value, and so on. None of those seem to be causing any problems for now, but we should consider cleaning that up in future releases.

wim leers’s picture

StatusFileSize
new41.64 KB
new910 bytes

One less failure.

(This time: filter_formats() again, called by \Drupal\filter\Plugin\DataType\FilterFormat::getSettableOptions(), called by \Drupal\Core\Validation\Plugin\Validation\Constraint\AllowedValuesConstraintValidator(), which uses the bubbling current_user service, and should really use the non-bubbling one. Form validation doesn't need render bubbling.)

wim leers’s picture

StatusFileSize
new40.27 KB
new1.42 KB

#41 caused us to go from 12 to 8 fails. But I said it fixed 5. It did. It also caused one regression, because one place (NodeBlockFunctionalTest) had been updated to expect the timezone cache context that no longer appears since #41.

This fixes that.

Together with #42, this should be down to 6 failures.

wim leers’s picture

StatusFileSize
new44.41 KB
new4.49 KB

Just like #42 fixed one validator to use the non-bubbling current user service, so should all validators be updated.

That, together with two more "save entity"-related things (in User::postSave() and Comment::preSave()) and one completely different thing that's completely independent from rendering (user_logout()), this should bring the number of failures down to zero.

wim leers’s picture

Next (and final) steps for the patch:

  1. import the "cacheability safeguard" mechanism/architecture from #2557815-88: Automatically bubble the "user.node_grants:$op" cache context in node_query_node_access_alter()
  2. tests that show it works as expected

Then it's purely down to reviews. And specifically, review whether this is worth doing, because I think it's clear this is impossible to make disruption-free. Also read #18.

dawehner’s picture

Is there any reason why we no longer pass along the user in things like the local task manager? Going from explicit calling to implicit is IMHO not an improvement. Sadly its not described in any comment on this issue, but I can't really believe that its a technical necessarity, because in case it is, this will be a huge confusing API for people.

Status: Needs review » Needs work
wim leers’s picture

It's described in #18:

However, I can reduce the pain there by stopping to inject the current user into many services; many services don't actually need it. Doing that next.

The access_manager already gets the current_user passed in. So, any service that calls the access_manager doesn't need to pass in the current_user: that's pointless by definition, because the access_manager already has it.

So I don't see at all what's confusing about those changes; in fact, I find it much cleaner. In HEAD, it's confusing why you need to pass the current_user again to the access_manager.

Did that make sense? If you disagree, can you explain why you find this less clear?

wim leers’s picture

StatusFileSize
new45.44 KB
new1.09 KB

One unit test had to be updated. Actually green now.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new24.81 KB
new24.3 KB

Discussed with @dawehner in IRC. He felt very strongly that we should be explicit, and keep injecting the current_user service into the contextual link/local task/location action managers.

So, reverting the changes in #20, #21 and #34.

Side benefit: this hugely reduces the size of the patch :)

wim leers’s picture

import the "cacheability safeguard" mechanism/architecture from #2557815-88: Automatically bubble the "user.node_grants:$op" cache context in node_query_node_access_alter()

I tried to do this, but failed miserably, because Symfony very much insists on making the inner/decorated service (current_user.non_bubbling in our case) private, and even trying tricks using aliases, I am not able to figure out how to convince Symfony to not do that.

Therefore, leaving the patch as-is for now.

wim leers’s picture

Assigned: wim leers » Unassigned
Issue tags: -sprint

Also, the most relevant snippet from an IRC conversation:

14:33:25 <catch> I really don't know about the patch in general.
14:33:51 <WimLeers> yeah me neither
14:34:26 <WimLeers> catch: dawehner So, one part that will *definitely* be affected in a negative manner are non-HTML routes
14:34:37 <WimLeers> Which you can tell by the number of test failures in REST routes
14:34:41 <WimLeers> which took the longest to debug
14:35:00 <WimLeers> Anything in contrib there using the current_user  service today will basically break
14:35:14 <WimLeers> because REST responses don't implement CacheableResponseInterface
14:35:28 <WimLeers> Which means EarlyRenderingControllerWrapperSubscriber will throw an exception for them
14:35:54 <WimLeers> We could in theory make EarlyRenderingControllerWrapperSubscriber put a *subclassed* RenderContext on there, and detect that in the bubbling current_user service, and if so, NOT bubble the cacheability metadata
14:36:02 <WimLeers> But then certain use cases *won't* be protected
14:36:09 <catch> WimLeers: tbh I think it is 8.1
14:36:18 <catch> And if we've had issues before then, then we can focus on it.
14:36:21 <WimLeers> i.e. any controller calling \Drupal::currentUser()
14:36:25 <catch> And if not, then leave it alone.
14:36:28 <WimLeers> okay
14:36:30 <WimLeers> thanks
14:36:33 <WimLeers> that's very helpful
wim leers’s picture

Version: 8.0.x-dev » 8.1.x-dev

#55 was around for >24 hours.

Moving to 8.1 per that IRC excerpt.

effulgentsia’s picture

I wonder if this is a better way of addressing the REST failures.

Here's a patch that goes back to #18 and just modifies EarlyRenderingControllerWrapperSubscriber a little. Interdiff is relative to #18.

effulgentsia’s picture

Version: 8.1.x-dev » 8.0.x-dev

#57 doesn't appear to be queued for testbot. I wonder if changing the Version will fix that.

effulgentsia’s picture

Maybe I need to reupload with that version already set?

[Edit: yep, that seemed to do it]

Status: Needs review » Needs work
effulgentsia’s picture

Status: Needs work » Needs review
StatusFileSize
new20.93 KB
new4.91 KB

Some fixes extracted from #53.

Status: Needs review » Needs work
effulgentsia’s picture

Status: Needs work » Needs review
StatusFileSize
new19.32 KB
new1.72 KB
+++ b/core/core.services.yml
@@ -595,13 +595,13 @@ services:
   plugin.manager.menu.local_action:
     class: Drupal\Core\Menu\LocalActionManager
-    arguments: ['@controller_resolver', '@request_stack', '@current_route_match', '@router.route_provider', '@module_handler', '@cache.discovery', '@language_manager', '@access_manager', '@current_user']
+    arguments: ['@controller_resolver', '@request_stack', '@current_route_match', '@router.route_provider', '@module_handler', '@cache.discovery', '@language_manager', '@access_manager', '@current_user.non_bubbling']
   plugin.manager.menu.local_task:
     class: Drupal\Core\Menu\LocalTaskManager
-    arguments: ['@controller_resolver', '@request_stack', '@current_route_match', '@router.route_provider', '@module_handler', '@cache.discovery', '@language_manager', '@access_manager', '@current_user']
+    arguments: ['@controller_resolver', '@request_stack', '@current_route_match', '@router.route_provider', '@module_handler', '@cache.discovery', '@language_manager', '@access_manager', '@current_user.non_bubbling']
   plugin.manager.menu.contextual_link:
     class: Drupal\Core\Menu\ContextualLinkManager
-    arguments: ['@controller_resolver', '@module_handler', '@cache.discovery', '@language_manager', '@access_manager', '@current_user', '@request_stack']
+    arguments: ['@controller_resolver', '@module_handler', '@cache.discovery', '@language_manager', '@access_manager', '@current_user.non_bubbling', '@request_stack']

I'm curious why these changes are needed, so throwing a patch with that reverted at testbot.

Status: Needs review » Needs work
effulgentsia’s picture

Status: Needs work » Needs review
StatusFileSize
new20.06 KB
new1.11 KB

Hah! #61 and #63 have the same failures, so the reversions in #63 seem ok.

Here's a fix for those last 2 failures.

effulgentsia’s picture

Meanwhile, seeing if we can revert some more.

Status: Needs review » Needs work
effulgentsia’s picture

Status: Needs work » Needs review
StatusFileSize
new19.11 KB
new1.31 KB

Here's a way to fix that.

effulgentsia’s picture

I like this approach more.

effulgentsia’s picture

Cleaner version.

Status: Needs review » Needs work
effulgentsia’s picture

Status: Needs work » Needs review
StatusFileSize
new22.36 KB
new2.15 KB

Status: Needs review » Needs work
effulgentsia’s picture

Status: Needs work » Needs review
Issue tags: +rc target triage
StatusFileSize
new23.81 KB
new4.7 KB

Here's some docs additions, but not fixes for the 2 failures in #73. I'm done for the night. If someone (probably @Wim Leers) wants to fix those failures and figure out what else is needed to push this along, that would be great. I think the patch is at a state where we should triage it again for inclusion prior to RC4, because I think some of the concerns from #55 are now alleviated.

Status: Needs review » Needs work
wim leers’s picture

EntityAccessControlHandler's "current user" service is used by UserAccessControlHandler to compare the user ID (for "edit/view own"-style permissions). Which means that we simply must use the non-bubbling service in EntityAccessControlHandler. Which is the right thing to do anyway, because:

  1. access checks may very well run independently of rendering, so it does not make sense to bubble cacheability metadata in that case
  2. access checks are already expected to specify cacheability metadata explicitly

This should make the patch green again.

wim leers’s picture

StatusFileSize
new21.19 KB
new3.98 KB

#77 AFAICT means that I should be able to revert all the getStaticCacheKey() stuff. Let's see if I'm right.

Status: Needs review » Needs work
wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new21.1 KB
new7.35 KB

I'm afraid I've got lots of bad news, news that in my opinion shows that we really can't do this.

(And news that unfortunately demonstrates how unfortunate it is that we didn't have (Refinable)CacheableDependencyInterface and the cache tags/contexts/max-age concepts since the very beginning of the Drupal 8 cycle.)


I firmly believe that the getStaticCacheKey() stuff added by @effulgentsia in #57 and later is the wrong direction. We'd be adding a public method without an interface. A method that should not even be necessary. Just to mask the bubbling in situations where bubbling should not happen in the first place.

The clearest validation possible can be found right in the consequences (test failures) of #78: even if we remove the static caching from filter_formats(), then we still pass in the bubbling current_user service to the access() method. By chance, it so happens that its id() method is also called only for static caching. But if the specific entity access logic would call id(), then it would still bubble the cacheability metadata!

So, it is absolutely and solely thanks to chance that #77 is green: if FilterFormatAccessControlHandler called id(), then it would be red! So any contrib module adding more advanced entity access logic will then still cause the cacheability metadata to be bubbled (inappropriately so!), despite not doing anything wrong whatsoever!


Similarly, all the reverts @effulgentsia made since #57, including the ones to the constraint validators and the Comment class (and to a lesser extent those in the access_manager's injected service)… they all just happen to only cause test failures for REST tests. So, sure, our tests are green. But that doesn't mean it is correct.

This also explains why @effulgentsia's surgical change to EarlyRenderingControllerWrapperSubscriber works: because validation happens on POST requests… hence it's possible to revert the validation changes I made.


Another example, that shows an even more painful/deeper example: the Node class's uid base field, which has

->setDefaultValueCallback('Drupal\node\Entity\Node::getCurrentUserId')

The default value callback customizes a Node object to vary by the current user. And we already have the infrastructure to solve this: RefinableCacheableDependencyInterface! But we don't use it here — it didn't exist yet at the time when Field API was designed.

In other words: we cannot ever cache the "add node" form correctly, because we fail to see that the Node object it depends on actually varies by user.


In conclusion, Drupal 8 has gotten much better about being aware about its dependencies. Without knowing about dependencies, it is impossible to correctly cache/invalidate things.

But… it's nowhere near done yet. We see problems like this at all layers, in all APIs: from the "default value callback" for a field definition, to the current_user service being injected not allowing us to conclude that the thing it is injected to must also vary by user, because the current_user service also allows you to vary something just by permission, by roles, by authenticated/anonymous.

Far too many objects still conflate many concerns into a single thing.

Far too much logic still assumes it's okay to access global state to figure out the current user/permissions/roles/….

Consequently, the only way I see something like this patch being workable/understandable, is by doing something like my patch was doing: inject the non-bubbling service when in fact we don't want bubbling to happen at all! My patch avoids doing bubbling cacheability metadata onto a render context when the thing happening is completely unrelated to rendering. This patch does not. But that approach also is brittle/confusing. Hence #55+#56 concluding this should be moved to 8.1.


I updated all places that use current_user.non_bubbling to have an explanation as to why they need it. They further support my explanation/analysis above.

  1. +++ b/core/core.services.yml
    @@ -1395,8 +1395,19 @@ services:
    +  # In most cases, use the 'current_user' service. The non_bubbling version
    +  # should only be used within code that must bypass the per-user cache
    +  # context getting added when a user-specific method is called. Such code
    +  # should be rare, and commented with an explanation for why the bypass is
    +  # necessary and safe.
    

    This is a bit dishonest; it doesn't mention at all this bubbling is specifically for rendering.

  2. +++ b/core/lib/Drupal/Core/Logger/LoggerChannelFactory.php
    @@ -43,7 +43,10 @@ public function get($channel) {
    +        // Loggers should be able to log the user who triggered a certain
    +        // condition during a cache miss, even for code execution that can
    +        // be cached across users, so use the non-bubbling service.
    

    This is a far too complex explanation IMO. Logs are not cached. There's simply no point in caring about cacheability metadata *at all*.

  3. +++ b/core/modules/node/src/Entity/Node.php
    @@ -500,7 +500,13 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +    // This method is only called for applying a default value to the uid
    +    // field. That is triggered even during requests that do not use the
    +    // field. Therefore use the non-bubbling service to prevent an unnecessary
    +    // cache context.
    +    // @todo Switch this to the regular 'current_user' service if/when default
    +    //   value callbacks are refactored to run lazily.
    

    This is only half the truth, see my comment for the full truth.

fabianx’s picture

#80: I did not read all, but would it help to distinguish between having an active RenderContext vs. having it not active?

e.g. its perfectly fine for #access to also bubble the metadata, in case it is used as part of a deeper render array. It is redundant but not wrong.

etc.

Status: Needs review » Needs work
effulgentsia’s picture

I still prefer the approach in #75. I think some of Wim's additions since then might be good, but I'd like to evaluate each one separately. Meanwhile, I think the 2 failures in #75 are actually the result of a bug in HEAD: #2614230: UserAccessControlHandler::checkAccess() fails to add a user cache context where needed.

effulgentsia’s picture

Status: Needs work » Needs review
StatusFileSize
new18.32 KB
new7.13 KB

For now, going back to #75, but Wim texted me an idea to move the isMethodSafe() check to inside CacheabilityBubblingAccountProxy, so this patch does that. Interdiff is relative to #75.

Status: Needs review » Needs work
effulgentsia’s picture

Version: 8.0.x-dev » 8.1.x-dev
Status: Needs work » Postponed
Issue tags: -rc target triage

I discussed this with @catch, @xjm, and @alexpott, and regrettably, we agree with Wim's earlier conclusion in #56 and again in #80 that this isn't viable for core in 8.0.

The following demonstrate some of the cases of false positives (i.e., a per-user cache context being bubbled where it shouldn't be and that therefore necessitates using the non-bubbling service or some other way of calling a per-user method without bubbling):

  1. +++ b/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php
    @@ -164,8 +166,9 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
    -    if (isset($this->accessCache[$account->id()][$cid][$langcode][$operation])) {
    +    $account_cid = CacheabilityBubblingAccountProxy::getStaticCacheKey($account);
    +    if (isset($this->accessCache[$account_cid][$cid][$langcode][$operation])) {
    

    Where the user's ID is only used for static caching, but by code that doesn't know if the result actually varies per-user. For example, in this case, the access result might only vary by permission, but we're statically caching per user anyway, because with static caches, we don't need to worry about this being inefficient, and it's actually more efficient to just statically cache in this way. This choice of a highly granular static cache strategy doesn't affect what is rendered. If the id() method is called as part of calculating an access result, that might matter, but simply calling id() to statically cache does not. So this patch allows for getting a static cache key without triggering the bubbling, and that's fine for core, but this is a category of false positives that contrib will encounter as well once this goes in, and thereby need to change their code accordingly. The patch also contains a similar example of this category in filter_formats().

  2. +++ b/core/modules/node/src/Entity/Node.php
    @@ -500,7 +500,13 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    -    return array(\Drupal::currentUser()->id());
    +    // This method is only called for applying a default value to the uid
    +    // field. That is triggered even during requests that do not use the
    +    // field. Therefore use the non-bubbling service to prevent an unnecessary
    +    // cache context.
    +    // @todo Switch this to the regular 'current_user' service if/when default
    +    //   value callbacks are refactored to run lazily.
    +    return array(\Drupal::service('current_user.non_bubbling')->id());
    

    Yet another category of false positives that contrib will hit upon.

  3. +++ b/core/modules/user/user.module
    @@ -1305,7 +1305,11 @@ function user_cookie_delete($cookie_name) {
    -  $user = \Drupal::currentUser();
    +  // We already explicitly set the necessary cacheability metadata, so we can
    +  // use the non-bubbling "current user" service. This way, if later running
    +  // code from other modules removes the per-user toolbar elements, the toolbar
    +  // can be cached across users.
    +  $user = \Drupal::service('current_user.non_bubbling');
    

    And another one.

  4. An example not in this patch, but that is quite easy to imagine, is a contrib module that adds additional fields to the user entity and then displays the value of those fields or something conditional on those values. The cache variation might only need to be per-whatever-that-field-value is, but there's no way to get at that value without triggering the bubbling (other than to explicitly use the non-bubbling service).

And a problem with encountering false positives like this is that it can bring a high traffic site down due to unnecessary cache variation.

On the other hand, not having this in 8.0 does risk module developers forgetting to associate the 'user' context where they should, and causing information disclosure vulnerabilities on sites. However, that's a problem that results from a bug in the module, whereas if this patch were to go in, the potentially site crippling performance problems would be due to a bug in core.

Given these factors and that the last RC is mere moments away, we decided to postpone this to 8.1.

However, we think this safeguard might be a very high value contrib project between 8.0 and 8.1, either as part of tooling during development to help catch places where a module developer might have made a mistake, or on production for highly security sensitive sites.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

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

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

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

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

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

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now 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.

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.

dpi’s picture

Title: Automatically assign user cache context & cache tag in current_user service » Automatically assign user cache contexts/tags when using current_user service
Component: user.module » cache system

Barely touches user.module.

moshe weitzman’s picture

However, we think this safeguard might be a very high value contrib project between 8.0 and 8.1, either as part of tooling during development to help catch places where a module developer might have made a mistake, or on production for highly security sensitive sites.

Anyone know of a contrib project like this? Or the opposite - catches situations where one tag starts appearing very often on your site and when invalidated, blows way more cache than anticipated.

berdir’s picture

Redis module has an issue/patch for a report page that among other general stats shows the render cache items with the most variations and most frequently invalidated cache tags. It does have some issues and can get very slow with enough tags though, which is why I haven't committed it yet.

#2848872: Add status page and some statistics

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.

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.

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

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.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.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.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.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.

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.

prudloff’s picture

If I understand #86 correctly, this was postponed until 8.1. So it can probably be reevaluated now.

prudloff’s picture

Status: Postponed » Needs work

xjm credited alexpott.

xjm credited catch.

xjm’s picture

Adding triage credits as per #86.

I discussed this with @catch, @xjm, and @alexpott, and regrettably, we agree with Wim's earlier conclusion in #56 and again in #80 that this isn't viable for core in 8.0.

It's definitely eligible to be considered now on that basis, although we'd want to evaluate whether the approach is still valid.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.