Problem/Motivation
If I want to implement a query access event subscriber for an entity type that doesn't already have a query_access handler (which includes all core entity types, since none of them do), then currently I need to also implement hook_entity_type_build() and assign that entity type a query access handler, because otherwise the entity.query_access.ENTITY_TYPE event is never dispatched.
Proposed resolution
Consider whether to assign all entity types a default query_access handler, so that modules can implement a query access subscriber for any entity type. I don't know whether it makes sense to do this in entity.module, or in a submodule. A submodule would allow sites to opt into that or not.
Remaining tasks
Decide if this makes sense or if it would introduce security concerns or other problems.
User interface changes
API changes
Data model changes
Release notes snippet
| Comment | File | Size | Author |
|---|---|---|---|
| #29 | entity-3086409-29.patch | 8.91 KB | kristiaanvandeneynde |
| #29 | interdiff-26-29.txt | 1.45 KB | kristiaanvandeneynde |
Comments
Comment #2
bojanz commentedI applaud the idea, but we've found this idea to be very problematic in practice.
Adding a query_access handler causes all queries to now be altered and respect permissions. When we did this for Commerce, it caused many bugs across the ecosystem, where existing entity queries had to be given ->accessCheck(FALSE). Examples of that include code that runs on cron or in queues (since the user might be anonymous) and custom load methods in entity storage classes (since storage should generally return entities unaffected by access). Plus the general user surprise, when permissions start being enforced where they previously weren't.
Considering how many contribs load and manage core entity types, all of these problems have the potential to be much worse than they were in Commerce.
Comment #3
kristiaanvandeneyndeFYI, going to create a light version of this in Group:
entity.query_access.{$entity_type_id}event to add Group's logic.Thought this idea might help anyone trying to get query access going on entity types they do not own so that their module might intervene.
Comment #4
kristiaanvandeneyndeCome to think of it, we could just as well do this in Entity API so that at least the alter event is fired for any entity type and the actual query_access handler does absolutely nothing but fire the event. At least this way, modules can benefit of the streamlined query altering system provided by Entity API and we wouldn't break core/contrib in the process.
Setting back to Active. If you disagree with this approach, please just close it again @bojanz :)
Comment #5
andypostI'd like to mention about set of existing domain entity modules - they use different hacks to make entities "domainable"
Comment #6
kristiaanvandeneyndeSomething as simple as:
With the generic class being:
Comment #7
kristiaanvandeneyndeNot sure we want to be testing this as it does not change anything functionally aside from entity types without a handler now having a basic "empty" one. We could write a test for a "test entity type" without a handler and then call $entity_type->hasHandler() on it to verify that we set the handler, I suppose.
Comment #8
kristiaanvandeneyndeHere's the patch from #7 that takes the change from #3134363: Support a catch-all QueryAccessEvent for modules that may want to alter all entity queries into account already, in case that issue gets committed first.
Comment #9
mglamanOne thing that would be interesting is to add a test which verifies the generic query access is added to Node, by providing a test module which subscribes to node query access.
Comment #10
kristiaanvandeneyndeYeah I was just thinking the same when we talked about these patches yesterday, will add that.
Comment #11
kristiaanvandeneyndeThis is the standalone patch, does not combo with the other issue.
Comment #13
kristiaanvandeneyndeForgot core has some interesting naming patterns on node entity types.
Comment #14
berdirhaven't looked at the entity query access api before, but why OR? Wouldn't that mean that if two separate modules implement the event, it would allow one *or* the other condition? That's not how access API's usually work.
The event also doesn't have a way to set a new condition group object, so you couldn't change it to an AND condition object even if you'd want to.
I did check that nothing happens if the conditions object is empty, so that's good.
adding this to an existing test and always doing it is going to be really confuse someone writing some other tests.
Usually entity/node access tests in core work with flags and/or dedicated modules. Lets add a \Drupal::state()->get('something') and enable it just for our specific test?
Comment #15
kristiaanvandeneyndeAFAICT: It tries to stay as close to node grants as possible so that the transition into core might be smoother. With node grants, it's also the case that if you have one key, you have access, regardless of the amount of locks. The difference here being that a handler could set isAlwaysFalse() to TRUE, which would make the query return no results.
The default conjunction of ConditionGroup is actually AND, but looking through the code, it's basically always instantiated with OR.
This part of the API has raised the same question for me, but I suppose if you allow it to be altered you could break other modules' access logic. You can nest groups, though. So if you need an AND conjunction, you can create your own ConditionGroup and add it to the main one.
Re #14.2
I'll have another look at the test
Comment #16
kristiaanvandeneyndeHere we go.
Comment #17
kristiaanvandeneyndeAnd this one makes it ready to be committed after #3134363: Support a catch-all QueryAccessEvent for modules that may want to alter all entity queries lands.
Comment #18
berdirNeeds a reroll.
Comment #19
mrinalini9 commentedRerolled patch #17, please review.
Comment #21
kristiaanvandeneyndeThe reroll in #19 is missing the subscriber. I actually already pre-emptively rerolled this issue in #17. I'll just re-attach the same patch from that comment.
Comment #22
kristiaanvandeneyndeHmm I see what was going on. Both patches used the same event subscriber method name. I changed the wording a bit, code remains the same functionally.
Comment #23
berdirI think this does implement it in a way that addresses the initial concerns from @bojanz, it won't do anything without someone actually implementing this. It does have a bit of a performance overhead, but hopefully not too bad compared to actually building the entity query.
A bit unsure about the naming here.
With this, we now have a QueryAccessHandlerBase with a ton of default logic, and we have a GenericQueryAccessHandler that's just doing the events.
Feels like it should be kind of the other way round, but it's too late to change that now.
Specifically, \Drupal\entity\QueryAccess\QueryAccessHandlerBase::getConditions() could be the shared base class with an empty buildConditions() method that a DefaultQueryAccess method could then implement with that owner logic and so on.
As I said, too late now here, but maybe something to consider if/when we port this to core.
What about at least naming this differently though, after it what actually does, something like EventOnlyQueryAccessHandler?
Thoughts?
For the comment, I'd suggest "default query_access handler" or so.
Slowly getting into this and wondering about two things now, considering that we learned that the query access logic is actually on the the SQL query layer.
a) It doesn't really make sense for config entities, it will never be called for them anyway. Lets skip them?
b) It will also only be called on content entities with an sql storage. Limit it to storages that implement \Drupal\Core\Entity\Sql\SqlEntityStorageInterface?
more naming fun.
This is just the test module, but still.
Her we now have generic, which actually maps to the logic of the base query handler, and the other one uses polyfill, but this doesn't fit my understanding of the word polyfill, it's also not used elsewhere. Maybe something with Default, or again the event only thing from above.
the word "just" should be avoided in documentation I think, also, "convenient" (for whom?) doesn't seem necessary, "This is an example" seems enough here?
Comment #24
kristiaanvandeneyndeRe 1. and 3. You're right, "generic" does not apply here as much as it did for the "fire the event for any entity type" issue. Let's rename it to EventOnly across the board. The base/default class change for core I completely agree with.
Re 2.a and 2.b: We should skip the config entities. The SQL one is already being checked for in ViewsQueryAlter/EntityQueryAlter, but I guess it makes sense to test it early to reduce overhead.
Re 4. copied that straight from the test of that test class.
I'll make changes to 1, 2 and 3 already and leave 4 up for you to decide.
Comment #25
kristiaanvandeneyndeThink I got everything
Comment #26
kristiaanvandeneyndeHmm forgot to address 2. Turns out I can accommodate 2a, but not 2b. As you're trying to load the storage during entity type compilation, meaning you'll end up in an endless loop by calling the entity type manager while entity types are still being built. So the checking for SQL storage needs to remain in the 2 QueryAlter classes.
Comment #27
berdirFor 2b, I think you should be able to use is_subclass_of($entity_type->getStorageClass(), SqlEntityStorageInterface::class).
Comment #28
berdirThat could possibly conflict on an entity type that has its storage altered later on, but is possibly something we can ignore.
Comment #29
kristiaanvandeneyndeOk, here we go. And I agree that we should not need to worry about people altering entity types.
Comment #30
berdirwas looking a bit more at the inner workings of the query alter stuff, not related to this issue directly, but posting it here for now as they were just in slack before, might be useful for the core issue eventually.
Same comments are still a bit suboptimal I think, but the class names at least are much better now, this makes more sense to me. Committed.
Comment #31
berdirComment #33
kristiaanvandeneyndeHuge thanks!
Re #30 cross-posting from Slack: