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

  1. Decide on a solution
  2. Ensure that all core entity queries specify accessCheck
  3. Code fix & test
  4. Write change record
  5. Review
  6. 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

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:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

catch created an issue. See original summary.

alexpott’s picture

We have similar issues with multilingual-ness it is too easy to forget about it.

alexpott’s picture

timmillwood’s picture

Is 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.

dawehner’s picture

@timmillwood
In D9 that sounds like a good idea, but for D8 we need some way of BC layer.

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.

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.

jonathanshaw’s picture

This 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?

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.

larowlan’s picture

How about we change it from


->getQuery();

to


->getQuery($access_check = NULL) 

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?

catch’s picture

Issue summary: View changes

Marked #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.

catch’s picture

Issue summary: View changes
jonathanshaw’s picture

Our slack conversation was:

@catch:

The deprecation would be less disruptive since existing accessCheck(FALSE) code would work fine, and it makes you think about it when you write the query.
The two services would make you decide up front, and let us document more which one is which, but seems like it would be easy to pick the wrong one/not realise the other one is there.
So overall requiring an accessCheck call on queries seems preferable, but only by a bit.

Referring to #15 I said:

As the query can technically be accessed in other ways than by getQuery() it would probably make sense to do something like the second option as well if we were doing the 3rd. i.e. require accessCheck() and have getQuery() add an accessCheck() for us if an optional boolean is provided. I'm not sure about whether encouraging getQuery(TRUE/FALSE) is a good idea though; it's convenient, but it's semantic is also opaque.

@alexpott said:

I think I favour the deprecation and making you think upfront rather than two services. I agree with the getQuery(‘OR’, TRUE) or getQuery(‘OR’, FALSE) looking odd… if only we had the new enums from PHP 8.1 :slightly_smiling_face:

@dww said:

I also think requiring the accessCheck() call is better than separate services. Easy to miss that the other service exists. But forcing authors to add the access check and have to pick a value seems like a good way forward. We should always consider access, so being required to do so is progress...

Also, if you got it wrong, easier to change your mind and s/FALSE/TRUE/ in that one spot than to change the whole service used for the query.

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.

jonathanshaw’s picture

Status: Active » Needs review

I'm hoping the test bot will identify all the places where we need to add an access check.

jonathanshaw’s picture

Issue summary: View changes

The 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.

if (is_null($this->accessCheck)) {
      @trigger_error('Omitting to explicitly specify whether access should be checked on an entity query is deprecated in drupal:9.2.0. An error will be thrown from drupal:10.0.0. See %cr-link%', E_USER_DEPRECATED);
}

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.

jonathanshaw’s picture

Residual 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.

longwave made their first commit to this issue’s fork.

longwave’s picture

Found the culprit for the page cache fails, also fixed MakeUniqueEntityFieldTest.

jonathanshaw’s picture

We have a change record now: https://www.drupal.org/node/3201242

Thanks for the review @longwave! And especially for fixing the caching test fail.

Are we going to throw a full deprecation here, but fall back to TRUE for now?

I have now restored the deprecation warning in place of temporary debugging code as we are now green

I think technically this could be $this->any() as it doesn't matter how many times it's called, it always returns the same thing.

This is overridden with FALSE two lines below.

Fixed

Is using TRUE in the examples setting a better precedent?

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.

Should this be FALSE?

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.

jonathanshaw’s picture

jonathanshaw’s picture

Reviewing 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

jonathanshaw’s picture

Fixed #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 be FALSE. Some items of concern there.

longwave’s picture

Status: Needs review » Needs work

NW for a deprecation test. After that I think this is ready for RTBC.

longwave’s picture

Status: Needs work » Reviewed & tested by the community

Thanks, looks good - RTBC if bot agrees.

catch’s picture

Issue summary: View changes

I 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.

jonathanshaw’s picture

Status: Reviewed & tested by the community » Postponed

jonathanshaw’s picture

I'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.

jonathanshaw’s picture

I don't understand the previous fails, trying a Gitlab rebase.

longwave’s picture

TaxonomyStorage::getParents() is missing an accessCheck(), and there are maybe some more after that too.

jonathanshaw’s picture

Thanks @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.

andypost’s picture

Status: Postponed » Needs work

All blockers are committed

jonathanshaw’s picture

Status: Needs work » Needs review

Let'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.

jonathanshaw’s picture

Issue summary: View changes

A 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.

jonathanshaw’s picture

A 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.

I was wrong about this. We just needed to rebase.

andypost’s picture

Polished CR as wrong interface been mentioned https://www.drupal.org/node/3201242/revisions/view/12260497/12271174

Hope this went green to RTBC!

jonathanshaw’s picture

+1 to @andypost's changes to code and CR.

andypost’s picture

Fixed last broken test

jonathanshaw’s picture

+1 to that last fix, thanks @andypost.

jonathanshaw’s picture

Changed 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"

longwave’s picture

Status: Needs review » Reviewed & tested by the community

As agreed in Slack the new message is good, this is ready to go I think assuming bot agrees.

catch’s picture

Issue tags: +Needs followup

Looks 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.

jonathanshaw’s picture

jonathanshaw’s picture

Issue tags: -Needs followup
catch’s picture

Whoops thank you, following that one now.

  • catch committed aea84a7 on 9.2.x
    Issue #2785449 by jonathanshaw, longwave, andypost, catch, timmillwood,...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Really nice to see this one RTBC after a long journey to get here.

Committed aea84a7 and pushed to 9.2.x. Thanks!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.