Problem/Motivation
If the current user does not have the permission to edit/delete any node of a given type, node_node_access() always adds the user cache context for the update/delete operation as part of checking the edit/delete own permissions.
There are however several cases where that's actually unnecessary, for example for users without the edit/delete own permission.
It could still end up being merged in if a user has the permission, but you can at least avoid it if no user has that permission.
This isn't a huge deal by default as it "just" results in the local tasks block being given the user cache context, but if you combine it with #2972308: Allow users to translate content they can edit, then through content_translation_language_fallback_candidates_entity_view_alter(), you can end up with the user cache context on the main content, disabling dynamic page cache in the process.
Proposed resolution
Check the permission first, do not add the user cache context if the user doesn't have the permission.
That will also result in fewer cached local tasks blocks, especially on sites with many users without edit/delete any permissions.
Remaining tasks
User interface changes
The local task block on node pages does no longer get placeholdered, which results in less changes/jumps of content.
API changes
Data model changes
Release notes snippet
Comment | File | Size | Author |
---|---|---|---|
#35 | 3016781-35-interdiff.txt | 831 bytes | Berdir |
#35 | 3016781-35.patch | 8.66 KB | Berdir |
#32 | 3016781-32.patch | 7.85 KB | catch |
#31 | interdiff-3016781-30.txt | 2.37 KB | catch |
#31 | 3016781-31.patch | 8.32 KB | catch |
Comments
Comment #2
BerdirThere's so much broken about this function...
This overlaps/conflicts with #2348203: hook_node_access() no longer fires for the 'create' operation as well as #2883553: Obsolete argument for hasPermission in node_node_access().
By converting to an elseif, we also need to add break and add the neutral case outside of the default.
And in the end, this function should simply not exist at all, node should not implement the hook for its own entity type, this logic belongs in \Drupal\node\NodeAccessControlHandler::checkAccess(). That would probably be too much for a single issue, though.
Comment #3
BerdirFor tests, we could maybe extend \Drupal\Tests\node\Kernel\NodeAccessTest to also check for the correct cacheablity metadata.
Comment #5
arpad.rozsa CreditAttribution: arpad.rozsa at MD Systems GmbH commentedAdded a test which asserts the correct cache contexts and fixed the failing ones.
Comment #6
arpad.rozsa CreditAttribution: arpad.rozsa at MD Systems GmbH commentedRe-uploading the test only patch, because it was corrupted.
Comment #7
arpad.rozsa CreditAttribution: arpad.rozsa at MD Systems GmbH commentedhttps://www.drupal.org/project/drupal/issues/2883553
This issue was committed yesterday, so rerolling my patch to make it work with the latest changes. Otherwise nothing changed in the patch.
Comment #8
arpad.rozsa CreditAttribution: arpad.rozsa at MD Systems GmbH commentedComment #9
Berdirone empty line too much here.
not needed.
This shows perfectly how this is a good thing.
The local tasks no longer have to be placeholdered/varied-by-user until there actually *is* a user with only edit own permission and not edit all.
Comment #10
arpad.rozsa CreditAttribution: arpad.rozsa at MD Systems GmbH commentedMade all the changes according to your feedback.
Comment #12
Wim LeersThe issue summary
This is not true, because:
If you enter the first branch, it cannot possibly vary by the
user
cache context.You're right though that in the second branch it always vary by user, even when the user doesn't have that permission! Great find! 👏 😀
Still, the claim in the very first sentence of the issue summary is incorrect. Explaining this more clearly will help this issue get committed much faster.
The patch
#2: Wow!
That's not what this is testing.
We could fix this by just adding the sole test method of this class to
\Drupal\Tests\node\Functional\NodeAccessAutoBubblingTest
and perhaps renaming that test class, toNodeAccessCacheabilityTest
.This means that the
user.permissions
cache context will incorrectly be missing if we don't enter theelseif
branch.This should assert that the
user.permissions
cache context is present.NICE!!! This means fewer things to render on every page load, and therefore a slightly faster Drupal!
It'd be nice if we'd have benchmarks for this and preferably even profiling, but that's not a requirement. It'd make for a cool release notes snippet :)
Great work! 🎉
Comment #13
Wim LeersActually, for #9.3 / #12.4, I'm bumping this to 🤘
It's not a major improvement in and of itself … but it has a performance (and a small scalability) impact on just about every Drupal 8 site. Which is why it'd be great to benchmark it.
Comment #14
kristiaanvandeneyndeAgree with Wim on the user.permissions cache context needing to be set in case of 'update' or 'delete'.
So we can actually keep the default case returning a plain AccessResult::neutral(), but the delete and update case need to look like this:
If we don't implement the fallback return value in the else statement and the first person to have their access checked happens to not have access, then we will cache a neutral result for everyone. So the tests definitely need to check this, as Wim said, to avoid regressions further down the line.
Comment #15
BerdirTrue, right now that that actually can't fail in any way because that context is always theret. We also can't test for it, because \Drupal\node\NodeAccessControlHandler::access() does always add that anyway, and we wouldn't just call this specific hook.
Also, I'd just change it to store the the access result in $access and then always call $access->addCacheContext() after the if/elseif.
Comment #16
Wim Leers+1
Comment #17
Berdir> It'd be nice if we'd have benchmarks for this and preferably even profiling, but that's not a requirement. It'd make for a cool release notes snippet :)
I did find this issue when profiling a project we're working on. With just core, it likely doesn't make a big difference, but as soon as you add node grants and a decent data set then things look very different, because having to check update/delete node access on every request can be pretty expensive then.
Comment #18
arpad.rozsa CreditAttribution: arpad.rozsa at MD Systems GmbH commentedMoved the added test method into the suggested class and made sure that the
user.permissions
cache contexts are always set, even if we don't enter theelseif
.Comment #20
BerdirUpdated issue summary to address the input from Wim, I also think the patch was updated based on the reviews.
The first patch here was written by me, but it changed quite a bit since then and has been reviewed by Wim. Setting to RTBC, but I'll also ping Wim to check this again.
Comment #21
catchOne very minor comment. Leaving RTBC (and also for Wim to have a last look).
Do we explicitly need this given the allowedIfHasPermission() just above? I guess we do in case that code changes but maybe a comment for people copying the logic?
Comment #22
jibranThis change reminds me of #3016038: Unrecognised entity operation passed to Menu Link Content throws exceptions.
Comment #24
Berdir"CI job missing"
@catch: Hm, we only use allowedIfHasPermission() in the 'create' case, update/delete doesn't. So technically, we do need that. It's still pretty theoretical, because NodeAccessControlHandler adds that to each access result anyway and user.permissions in the end is a required cache context, but is the correct thing to do.
Comment #26
Mixologicdispatcher issues. returning to rtbc
Comment #28
catch@berdir OK that's a good point, but then why not use AccessResult::allowedIfHasPermission() in these cases too?
Comment #29
Berdir@catch: And where exactly would we call that? We first do an elseif ($account->hasPermission('edit own ' . $type . ' content')), but the reason for doing that is that we always want to have that cache context, whether or not that condition is TRUE, that's why it was separated out.
Comment #30
BerdirSetting back to RTBC because I don't see how see how that would make the patch easier. We'd need to assign $access inside the elseif () and then merge it with an andIf() inside the condition.
Again, adding it at all is purely theoretical anyway, it's always there anyway because NodeAccessControlHandler adds it to any access result it returns.
Comment #31
catchOK I'm not sure if I'm not explaining my idea well, or if I am and you just think it's a bad one. So here it is. Passes the new test.
Comment #32
catchThis one should apply.
Comment #34
BerdirI see, the orIf() part is a bit strange as you already know that access isn't allowed from that, we only do that to keep the cache context. But I like it.
the test fail might be a new test, will check.
Comment #35
BerdirOk, the test isn't new, this actually does result in exposing that message, which seems like a good thing, even though it is not the only way you could have access.
It was however confusing that the expected/actual was inverted, fixing that by using assertSame().
Comment #36
Wim Leers🎉 — this is a nice side effect of using
AccessResult
and friends in the intended way :)Excellent! Thanks @catch :) Like @Berdir, I like this too!
Excellent pattern to follow elsewhere too.
👍— much more accurate name indeed.
👌
🥳— this is showing all the big cacheability wins!
Comment #37
alexpottCommitted and pushed 039eae1719 to 8.8.x and cd253e6860 to 8.7.x. Thanks!