Problem/Motivation
#2785155: _node_access_rebuild_batch_operation uses queries that check access shows how easy it is to forget to use accessCheck(FALSE) when writing entity queries that need to ignore access. In this case when rebuilding the very table that does the access checks (when access checking is likely to be foremost in your mind).
I've run into this on site builds too - i.e. if you forget to do this in an update, then you can end up not updating anything on sites where anonymous users can't access anything and updates are run via drush - unless you run the update as user/1 from update.php in which case it'll run OK.
The result of a bad query can often be data-integrity issues - i.e. an incomplete node grants table. Also not only is it easy to forget accessCheck(FALSE) when writing queries, but in many cases that code will run fine (because it's written on an install not using node grants), then fail when node access is introduced.
Defaulting access checks to TRUE was done to avoid information disclosure bugs, but this has been at the cost of making it very easy to introduce data integrity bugs instead.
Proposed resolution
Require an ::accessCheck() call when building a query, deprecate not calling it.
To do this, we had to fix all existing entity query uses in core in #3200985: [meta] Fix undesirable access checking on entity query usages.
Alternative resolutions that we considered
1. Move the decision about access checking earlier, by having an entity query that by default has accessCheck(FALSE). This is potentially just an extra factory and service (entity.query.all_access? entity.query.no_access_check?) that would add accessCheck(FALSE) to the query object before returning it from the factory.
Most classes will either being doing audit-type queries so can inject the different service or only rendering so can use the current one. Ones that are using both can use both services separately. Either way this ought to be easier to keep track of than checking every query for whether or not it needs an accessCheck(FALSE) or not.
2. Add an $access_check parameter to ::getQuery(), and deprecate not calling that.
Remaining tasks
- Decide on a solution
- Ensure that all core entity queries specify accessCheck
- Code fix & test
- Write change record
- Review
- Commit
User interface changes
None
API changes
The accessCheck() method must be called on an entity query.
Data model changes
None
Issue fork drupal-2785449
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
alexpottWe have similar issues with multilingual-ness it is too easy to forget about it.
Comment #3
alexpottAnd here's the example that prompted this issue... #2785155: _node_access_rebuild_batch_operation uses queries that check access
Comment #4
timmillwoodIs using one service for access check, and a different one for no access check any different to adding
accessCheck(FALSE)
, other than you need to specifically pick with or without access check, ie there is no default.Comment #5
dawehner@timmillwood
In D9 that sounds like a good idea, but for D8 we need some way of BC layer.
Comment #13
jonathanshawThis bites me again and again. And others. It seems like leaving a live electric wire around for children to play with, just asking for trouble.
How about we deprecate the default value and trigger a deprecation warning if accessCheck is not specified on the query?
Comment #15
larowlanHow about we change it from
to
i.e. make it an upfront decision
And if $access_check is NULL, we default it to TRUE and trigger a warning that says creating an entity query without specifying a value for access check is deprecated in D9 and will break in D10?
Comment #16
catchMarked #2931028: Separate entity query service into access checked and not access checked as duplicate.
Documented the three proposed solutions for this issue in the issue summary.
Comment #17
catchComment #18
jonathanshawOur slack conversation was:
@catch:
Referring to #15 I said:
@alexpott said:
@dww said:
Overall it seems like there's a consensus for requiring the access check to be specified in some form on a single query service rather than splitting into multiple query services.
Comment #20
jonathanshawI'm hoping the test bot will identify all the places where we need to add an access check.
Comment #21
jonathanshawThe fix I'm proposing is that in core/lib/Drupal/Core/Entity/Query/QueryBase.php we default the accessCheck property to null
protected $accessCheck = NULL;
and then in core/lib/Drupal/Core/Entity/Query/Sql/Query.php we make sure it's not null i.e. it's been specified as a boolean.
Temporarily on the MR I've got it triggering an exception no a deprecation warning, as the exception gives a stack trace on Drupal CI that is useful for running down test failures.
Comment #22
jonathanshawResidual test fails are all of one kind to do with page cache tags. I may have missed one last accessCheck() needed somewhere, but I've no idea where.
Comment #24
longwaveFound the culprit for the page cache fails, also fixed MakeUniqueEntityFieldTest.
Comment #25
jonathanshawWe have a change record now: https://www.drupal.org/node/3201242
Thanks for the review @longwave! And especially for fixing the caching test fail.
I have now restored the deprecation warning in place of temporary debugging code as we are now green
Fixed
I do not believe so. I think that the reason we have this whole pickle in the first place is that defaulting to TRUE is not what developers expect or normally need in an API query class; FALSE is more commonly what we want when we're doing backend code.
But it certainly doesn't matter to me and feel free to change to FALSE if you're convinced that's better.
I will add these cases to the follow-up #3200985: [meta] Fix undesirable access checking on entity query usages. This is a significant change to DX, and touches 100+ files, so for reasons of issue management and sanity preservation I think it's vital that in this issue we don't change the behavior of any current query.
====
I believe this is complete and now ready for RTBC. I will create a follow-up issue for 10.x to change the deprecation warning to an exception.
Comment #26
jonathanshawI created a follow-up on 10.x to go from a deprecation to an exception: #3201287: Throw an exception if accessCheck is not called on an entity query
Comment #27
jonathanshawReviewing this closely, I suspect that the following queries involve config entities and don't need the accessCheck added in the MR:
core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php
core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php removeSectionField
core/modules/media_library/src/Plugin/CKEditorPlugin/DrupalMediaLibrary.php
core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
core/modules/serialization/src/Normalizer/FieldableEntityNormalizerTrait.php extractBundleData
Comment #28
jonathanshawFixed #27.
I've listed in #3200985: [meta] Fix undesirable access checking on entity query usages all the places where we are setting
accessCheck(TRUE)
but it should probably beFALSE
. Some items of concern there.Comment #29
longwaveNW for a deprecation test. After that I think this is ready for RTBC.
Comment #30
longwaveThanks, looks good - RTBC if bot agrees.
Comment #31
catchI kind of think we might want to do #3200985: [meta] Fix undesirable access checking on entity query usages before this issue since:
1. We then wouldn't have to change those queries at all in this issue, just the ones we want to keep the same.
2. Anyone coming across the new deprecation message will find good examples of which way to go in core, instead of the opposite of what we want them to do.
Comment #32
jonathanshawPer #31 this is now postponed on #3200985: [meta] Fix undesirable access checking on entity query usages.
Comment #34
jonathanshawI'm preparing a verison 2 of the MR. First step is just a test version that throws an exception, to see if we have any outstanding queries in core where accessCheck is not specified, which we have missed from #3200985: [meta] Fix undesirable access checking on entity query usages.
Comment #35
jonathanshawI don't understand the previous fails, trying a Gitlab rebase.
Comment #36
longwaveTaxonomyStorage::getParents() is missing an accessCheck(), and there are maybe some more after that too.
Comment #37
jonathanshawThanks @longwave, really appreciated. Looks like I missed TermStorage and VocabularyStorage when creating the meta. I've temporarily fixed them here, let's see where that gets us.
Comment #38
andypostAll blockers are committed
Comment #40
jonathanshawLet's get agreement on RTBC for this in principle regardless of the test fails, because we might be adding new entity query uses to the core codebase so fast that by the time this gets reviewed the tests will be failing again. I'm working on the test fails in child issues of #3200985: [meta] Fix undesirable access checking on entity query usages.
Comment #41
jonathanshawA single case of a missing accessCheck slipped through in EntityQueryAggregateTest, I suggest fixing it here otherwise new cases will get added to core faster than we can find and fix them.
Comment #42
jonathanshawI was wrong about this. We just needed to rebase.
Comment #43
andypostPolished CR as wrong interface been mentioned https://www.drupal.org/node/3201242/revisions/view/12260497/12271174
Hope this went green to RTBC!
Comment #44
jonathanshaw+1 to @andypost's changes to code and CR.
Comment #45
andypostFixed last broken test
Comment #46
jonathanshaw+1 to that last fix, thanks @andypost.
Comment #47
jonathanshawChanged deprecation message to "Relying on entity queries to check access by default is deprecated in drupal:9.2.0 and an error will be thrown from drupal:10.0.0. Call \Drupal\Core\Entity\Query\QueryInterface::accessCheck() with TRUE or FALSE to specify whether access should be checked. See https://www.drupal.org/node/3201242"
Comment #48
longwaveAs agreed in Slack the new message is good, this is ready to go I think assuming bot agrees.
Comment #49
catchLooks solid now.
We'll need a 10.0.x follow-up to throw an exception instead of the trigger_error() since this isn't a bc layer as such.
Comment #50
jonathanshawWe have it already: #3201287: Throw an exception if accessCheck is not called on an entity query
Comment #51
jonathanshawComment #52
catchWhoops thank you, following that one now.
Comment #54
catchReally nice to see this one RTBC after a long journey to get here.
Committed aea84a7 and pushed to 9.2.x. Thanks!