Entity types need query-level filtering which restricts the list of entities to the ones that the current user can access.
In Drupal core this is done for nodes by the Node Access API, which joins a separate node grant table and uses it to determine access. In Commerce 1.x for D7 a simpler approach is used, the query is altered based on the granted permissions, and type/uid conditions are added to restrict the list. For example, if the user only has access to view own entities, the query receives a uid condition matching the current user's ID. If the user only has access to view specific bundles, the query receives a type condition for those bundles. This covers the full permission set, therefore covering the majority use case, without the need to (re)build a large grant table.
For Drupal 8 we've decided to implement an alter-based API, which parallels the permission providers and access control handlers also provided by this module. Fundamentally, query access is tied to permissions, so we need a query access handler for each permission provider.
Commerce 1.x implementation:
https://github.com/drupalcommerce/commerce/blob/7.x-1.x/commerce.module#...
https://github.com/drupalcommerce/commerce/blob/7.x-1.x/commerce.module#...
Architecture:
- ConditionGroup and Condition objects, holding the access logic (E.g. "uid = :uid AND type IN ([:type1])")
- The notion of a "query_access" handler, specified in the entity type annotation. The handler builds a set of conditions based on the current user's permissions.
- QueryAccessHandler (for EntityPermissionProvider) and UncacheableQueryAccessHandler (for UncacheableEntityPermissionProvider)
- A SqlQueryAlter class which applies the conditions to an SQL query, covering Views, Entity Query, Entity reference. In the future a parallel class could be introduced for Search API.
Note: Entity Query doesn't have a generic alter, so we have to alter it on the SQL level just like a Views query.
Current work: https://github.com/fago/entity/pull/52
| Comment | File | Size | Author |
|---|---|---|---|
| #22 | 2909970-22-query-access.patch | 124.53 KB | bojanz |
| #20 | 2909970-20-query-access.patch | 124.06 KB | bojanz |
Comments
Comment #2
bojanz commentedThis work wasn't completed in time for beta1 cause we got blocked on the permission side, which is now fixed: http://cgit.drupalcode.org/entity/commit/?id=9e60a18
The PR needs to be rebased on top of that work.
Comment #3
bojanz commentedComment #4
berdirThere's a core issue about completely revamping the node access grants system. Based on what I've seen it seems like a huge task that isn't likely to get done any time soon, but wanted to mention it as related: #777578: Add an entity query access API and deprecate hook_query_ENTITY_TYPE_access_alter()
Comment #5
bojanz commentedI'm back on this task. Reuploaded the PR to my brain, now to do something with it.
Comment #6
wim leersA lot of work has happened on #777578: Add an entity query access API and deprecate hook_query_ENTITY_TYPE_access_alter() until a few months ago. Any chance you can continue that work, @bojanz? Or is there a reason to not build on top of that?
P.S.: Also closed #2499645: Start using the entity query access API on orders, products and stores for you :)
Comment #7
bojanz commentedI started working on query access, then got distracted by permission provider bugs, and tagging a beta4 of Entity API.
The following issues got fixed:
#2978943: UncacheableEntityPermissionProvider generates incorrect view permissions for entities without an owner
#2977231: EntityPermissionProvider should provide per-bundle view permissions
Both required changes to the query access handlers.
With that out of the way, I resumed work on query access.
A new PR is up at https://github.com/fago/entity/pull/52, check out the notes there before reviewing.
Also note that the purpose of this api is to control access to the main entity type in the query. It is not feasible to control access to related entity types. For example, In commerce, products belong to a store. We limit the products list based on the permissions, but the system could construct a query that shows products based on a store that the user doesn't have access to. It is up to the parent module to ensure that this is safe (by controlling access to stores before doing the products query, for example).
@Wim Leers
I reviewed the state of that issue before resuming work here, but unfortunately, none of the work in that issue went in the right direction. Query access is fundamentally tied to permissions. This is why we're introducing query_access handlers that parallel permission_providers. Core needs to add permission providers to core, and then pursue a variant of the code being made in this issue.
Comment #8
bojanz commentedUpdating issue summary.
As mentioned in the PR, our three immediate TODOs are:
1. Add back code for "view own unpublished $entity_type", it got removed in the refactor.
2. Expand query access handler tests to cover entity types without an owner (the one provided by entity_module_test, for example).
3. Convert SqlQueryAlter into a service and give it an interface, to allow it to be swapped.
Comment #9
bojanz commentedComment #10
wim leersFirst: I'm SUPER duper excited about you working on this! 😀🤘🎉🎉🎉🎉 Bringing your years of experience to this subject area is certainly going to help move things forward!
I read/reviewed all of your commits since June. Most commits are "just" bug fixes. There's many more than the two listed in #7. I liked #2977318: Don't generate the "access overview" permission if the entity type has no collection link template and #2943571: Provide a BundleEntityAccessControlHandler with support for the "view label" operation a lot :)
That's not very constructive. You could absolutely be right, but it'd be great if you could elaborate on why you make that statement. You posted two comments on that issue, both on February 17, 2017. Nearly 16 months ago.
It very well might be obvious to you why that issue went in the wrong direction, but it's not to me. It's important that the community can read the rationale backing up a certain architecture!
Perhaps you could update the IS to explain how and why this is different than #777578: Add an entity query access API and deprecate hook_query_ENTITY_TYPE_access_alter()?
The IS update in #8 already is awesome BTW!
I started reviewing https://github.com/fago/entity/pull/52/files, but will need some more time to really grok its implications.
Comment #11
gabesulliceI reviewed the PR on GitHub here: https://github.com/fago/entity/pull/52#pullrequestreview-128082590
There, I promised to give a more high-level review on Drupal.org, which is what follows.
Overall, this looks nearly impeccable. I love how elegant this is WRT altering both entity queries and Views queries. FANTASTIC job.
I agree with your decision to forgo the core
Conditionclass, it is sufficiently different from a database condition to ignore the core version. That's the same conclusion that we reached in JSON API, GraphQL and the Entity Access Policies module (built to pilot #777578).I think any further comments I have will all fundamentally depend on your answer to:
I have to agree with @Wim Leers point here. The differences in approach are very clear to me after reviewing, but I'm not sure which of those (or perhaps all of them) is what "went in the wrong direction". I know a lot has changed since your last comment in #97.
Before I write any more, let me first say that far too much time has passed for me to take a prideful stance on the code written in #777578: Add an entity query access API and deprecate hook_query_ENTITY_TYPE_access_alter(), I would gladly burn it all in order to get something added to Drupal core for these kinds of use cases.
Please know that I'm just trying to be sure that we're not "throwing the baby out with the bathwater" in either direction.
Since I'm not sure to what you're referring about "wrong direction", this is what I see that is different, better and less capable between the two approaches:
First, I'm genuinely impressed with the SQL-level work done in this patch that is not implemented in #777578. That said, the work done here could be very easily incorporated there without any fundamental changes—in fact, we share nearly identical classes WRT to conditionsand groups and we both have caching logic associated with them.
I think what you're most likely referring to in #777578 is that its approach decided not to forego runtime access checks per entity. IOW, it is not based on queries alone. Personally, I agree with your decision to ignore them. Those could easily and quickly be removed from #777578 and I would support that move.
If my recollection serves me well, @kristiaanvandeneynde was most strongly in favor of keeping runtime checks. His rationale came from his experience with the Group module... he can probably make that case better than I would be able to.
If it's not runtime checks that went in the wrong direction, then perhaps it's scope?
AFAICT, #777578 implements a complete superset of the functionality offered by this patch. Along with extending query-level access checks to all entity types, we decided that a policy-based system would be a leap forward for Drupal on the whole (the existing permissions-based system can be implemented as one of many other policies). That would permit more finely attenuated access control rules for all kinds of applications. That work was done in YAML and config so that non-developers might find access control a more approachable task and in order to enable a second phase of UI development.
Other than that, while this patch has an open question WRT to supporting operations other than
view, #777578 chose to support those.In my mind, what would be the ideal is to merge the SQL-processing work implemented here with the policy-based approach in #777578. That would include keeping the other operations allowed there, but we would drop the "post-query" phase of access checking.
TL;DR:
Comment #12
bojanz commentedThank you for the initial reviews, Wim and Gabe!
Sorry, this was not my intention. Let me try again. My key point was "Query access is fundamentally tied to permissions."
Let's recap how this API works:
1) Each entity_type has a query access handler.
2) This query access handler checks the user's permissions and returns a set of conditions, generic value objects that can be applied to the query.
3) SqlQueryAlter takes the conditions, and applies them to the SQL query.
Note that #2 covers every permission that the permission providers provide. We vary based on entity owners and entity statuses (published). We handle both entity types that want per-user (any/own) views permissions, and those that don't (for smaller caches). All of this is constructed at runtime, zero cost.
Now let's look at how entity_access_policies works:
1) EntityAccessPolicyHandler is called. This class has a Handler suffix but I don't think that the intention is to provide it per entity type, it feels like a single entry point.
2) All "entity_access_policy" config entities are loaded into memory, then filtered by entity type.
3) The field condition plugins of each policy are called to provide conditions.
4) The conditions are applied to the query (not yet implemented).
This approach has the following smaller problems:
1) Complexity. We have an entity type and three plugin types (precondition, field condition, post condition) for the same use case. Significantly more code. No tests yet, though that's temporary.
2) Performance. We are loading all access policies for all entity types. In Commerce word, that means dozens and dozens.
3) Developer experience. As a module maintainer I can't just add a single query_access handler line to my annotation, I need to make sure I provide a config entity in my config/install, and keep it up to date if the logic changes.
Which brings us to the fundamental problem I mentioned above. Query access is completely tied to permissions and the underlying access control handlers. The logic between the two (query access and normal access) must match.
But entity_access_policy takes a different approach. Search the module for the word "permission", and you won't find any real mention. There is no attempt to generate conditions based on the available permissions, a config entity needs to be defined by hand, along with related plugins, taking into account all of the complexities. Our own query_access handler has about 8 different conditions/condition groups. Writing these as a part of the PR, testing them via kernel tests, ensuring that the logic completely matches the access control handler, this hasn't been trouble free at all, there are plenty of ways to trip up. Which is why I think that it's wrong to put the weight on the module developer, and not on the API itself. Now that I have the full complexity of normal access checking in my head, I never want another developer to go through the same pains.
I respect the work that was done, and understand the difficulty, I myself have attacked this problem unsuccessfully several times since 2013. But I can't agree with the idea that entity_access_policy is a superset of the current approach.
I completely understand this. But it's also obvious that the scope of a full access policy system, which does query access along the way, is much larger than the scope of an API that just does query access, following existing patterns, introducing no entity types or plugin types.
We fully support other operations, the open question is whether that code is worth having (maintaining), that is all.
Comment #13
gabesulliceWow, thanks for the very quick, thorough reply :)
This is the beginning of an insightful patch review :) It's clear you looked at it pretty thoroughly, would you consider posting more details in the module issue queue?
It's fair to say that the latter is beyond the scope of what your patch does, but it's hard to claim that one is a poorer experience than the other if they are accomplishing entirely different things. #777578 intentionally decouples access handling from the entity type definition and makes it an orthogonal concern.
This is what this patch accomplishes, it's not a present fact. Query access is not yet implemented for anything but nodes. It's also tautological, Drupal's current access system is permission based, therefore access is tied to permissions.
There are two condition plugins implemented, one for a handling permissions and one for handling published status. They do not need to be implemented again because they have been implemented once. I think this reveals a misunderstanding you may have about #777578—an entity type developer does not need to implement all the plugins, policies or conditions for their entity type or any at all.
The idea is that custom conditions can be developed for your entity type or any entity type. Thus a module named
commerce_business_hourscould be developed separately fromcommercewith a custom condition for "current hour >:begin & <:end & day IN :days" and that module would add a policy of "allow `buy` if business is open" with their own configuration page for business hours. This would not need to be implemented by the module maintainer of theproductentity type and the author ofcommerce_business_hourswould not need to swap the query access control handler out with their own.I suspect that scope is the biggest element of this, which is okay. Don't forget that I said, "I would gladly burn [entity access policies] in order to get something added to Drupal core for these kinds of use cases. Please know that I'm just trying to be sure that we're not 'throwing the baby out with the bathwater' in either direction."
Since there are a few obvious misunderstandings about #777578 in #12, let's just be sure we all know what we're trading off in all regards.
Comment #14
bojanz commentedExactly! The needs for entity type query access are different from the needs of site-builder-defined access. That's why we've built very different APIs.
Fundamentally we have two layers of conditions here:
1) The base access conditions: (bundle IN [X, Y] AND status = 1) OR (uid = 1 AND bundle = Z)
2) The additional user-defined conditions: current hour >:begin & <:end & day IN :days
This issue optimizes for #1 and doesn't touch #2.
The entity_access_policies module optimizes for #2 and supports #1 in theory.
The logic that we have in QueryAccessHandler isn't implemented in entity_access_policies.
That logic would require a custom plugin in entity_access_policies, because it checks permissions and generates conditions based on dynamic parameters (whether the entity implements EntityOwnerInterface, EntityPublishedInterface). The only way to avoid having that plugin would be to hardcode the checked permissions and their conditions, which then brings us back to having to redefine (and keep up to date) an access policy for each entity type. I urge you to try and implement an entity type's full set of conditions in a YAML file. It is non-trivial, and every mistake leaves content open to unexpected users, due to a mismatch with the main access handler.
My conclusion is that for entity_access_policies to support #1 properly, it would need to reimplement this PR, by having a single plugin responsible for generating the base conditions. The plugin would look at the entity type to decide whether to apply. Entity types with non-standard permissions (such as node) would provide their own plugin.
At that point you'd be right, and entity_access_policies would be a superset of the current work. It is not right now, but has the potential to be. However, I fear that the additional complexity and features that it brings make it very hard for it to land in core. It would be much easier to land #1 and provide an extension point for #2 (custom conditions generated via policies) instead.
Other responses:
It's just my train of thought as I go through the flow, sorry if it derailed the main conversation.
Well, it is architectural because config entities can't be filtered without loading them. Changing this requires replacing the entity type with a different approach.
Note that we discovered that Entity Query also needs to be altered on the SQL level, like Views, because it doesn't offer a generic (non-SQL-level) alter right now. This can be fixed in core, of course. That's why nothing's currently calling limitQueryByAccess() in entity_access_policies.
Comment #15
gabesulliceThanks for bearing with me on that discussion. I see where you're coming from, and you're probably right :). I agree with the bold statement then! (emphasis mine)
Let's try to get this to completion and into core ASAP! I think a lot of my review of the PR on GH is still valid.
Comment #16
wim leersI get the warm fuzzies when I see how technically deep, constructively critical, mutually respectful the discussion here is! ❤️❤️❤️
@bojanz++
@gabesullice++
I think #11 through #15 may be one of my favorite Drupal/open source consensus building moments ever!
This appears to capture succinctly why both issues have unique approaches!
Comment #17
kristiaanvandeneyndeBefore I go into detail below, let me first state that I love the discussion going on here and will look at the latest code on Github shortly. I think we can come to a consensus on first reworking the query level and then focusing on the rest. That said, I feel like there are a few items I need to address to avoid a situation where certain modules or use cases are overlooked and left with a system that doesn't work for them.
I see how you would get to this conclusion but you seem to be overlooking several actual use cases that have modules written for them:
While some of the above may still be solved by altering a query (e.g. Domain Access), they are far from tied to permissions with the possible exception of a "bypass all of these access checks" permission. So simply building this system on top of the permission layer seems ill-advised. (I'll have a look at the code and see whether this is actually the case.)
Furthermore, there are other modules which would rather not have a query run at all or are too complex to properly capture in a performant query. A business-hours-only module would, for example, prefer to completely cancel any entity query while the office is closed. This is what we tried to solve in EAP by introducing preconditions.
Then there's heavy-lifting modules, such as Group, which could probably function with altering entity queries but would have to resort to some god-awful queries to get the desired result. This is why I initially was vehement about having postconditions, but I have since come up with quite a few drawbacks to such an approach. A major drawback being the death of pagers as we know it. I'll go into more detail on these drawbacks during my session on this very subject at Drupal Europe.
To give you an example: Given a list of 20 nodes that you want to view with the Group module enabled, checking access for each one individually would be rather fast. But if any of them denied access, you'd end up with fewer than 20 (which is the pager drawback I mentioned above).
If, however, you'd tried to only get accessible nodes from the query, the query alter would have to add something like this:
INNER JOIN {group_content} gc WHERE node.nid = gc.entity_id AND (gc.gid IN ***VAR1*** OR (gc.gid NOT IN ***VAR1*** AND gc.type IN ***VAR2***))Where VAR1 is all of the groups the user is a member in a way that it would grant him access and VAR2 is all of the relation types that would grant access to people who are not part of the owning group. This does not even take authorship into account and neither does it account for some other higher-level permissions. The resulting (huge) query might work and actually perform well, but then again it might do horribly depending on your site set-up (how many groups, group types, etc.)
Please forgive the slightly technical segue above. The goal of it was to show that, sometimes, simply altering queries might not work well at all. I do not expect the result of this issue to actually tackle the problem outlined above, though.
That said, having some form of opt-out to cancel queries altogether (see office hours example) would be a great asset if you ask me.
Comment #18
bojanz commented@kristiaanvandeneynde
There is a QueryAccessEvent that allows modules to add conditions that are not permission based (group/domain/etc).
That should cover some of the additional use cases.
That said, we'll never cover everyone's use cases with this API. I'm sure there will still be a need for some kind of "Access Policies" module in contrib.
Comment #19
kristiaanvandeneyndeYeah I agree.
The only downside I see to QueryAccessEvent is that modules like Domain Access, Group, etc. might need to fit all of their logic in an event handler, rather than the entity type's query access handler. But it still beats hooks and/or swapping out the handlers (which could lead to a fight over which module's handler has the final say).
I see my concern of short-circuiting a query is already implemented:
I'd rather have the query not run at all, but this will do nicely I suppose. Doesn't SQL optimize this anyway?
Either way, I've reviewed the code on GitHub and it looks really promising. Nice work! I'll make sure to add this to my slides for next week.
Comment #20
bojanz commentedHere's the (hopefully) final query access patch, matching what's in the PR.
I am happy with the current test coverage, doing some manual tests on a Commerce site now.
Comment #22
bojanz commentedSo much for that.
Comment #23
kristiaanvandeneyndeI'd like to add in the provider info I talked about on the PR. Should I do that here or on Github?
Comment #24
bojanz commented@kristiaanvandeneynde
Let's do a followup issue with a patch, here in the queue.
I tried to do it in the main PR but ran into problems (Condition VS ConditionGroup, ConditionGroup::addCondition() DX).
Comment #25
kristiaanvandeneyndeSounds good! Just want to make sure we don't shoot ourselves in the foot w.r.t. BC by committing something here that is hard to expand with metadata (such as the provider) later on.
Comment #27
bojanz commentedComment #29
bojanz commentedManual tests on a Commerce install were successful. Going ahead and committing #22.
Of course, this code will keep evolving. I still welcome reviews. Open a new issue called "Review the Query Access API" and continue there.
Thanks, everyone!
Comment #30
wim leers🎉
Comment #31
kristiaanvandeneynde🎉
Comment #33
kingdutchThis issue flew me by! Very excited to see this land : ) I realise it's not great form to comment on closed issues but I had the exact same thing in mind about the great discussion here as Wim mentioned in #16!
🎉