Problem/Motivation
This is a sister issue of #2869426: EntityResource should add _entity_access requirement to REST routes, which is for the core rest.module. Unlike the REST module, the JSON API module only operates on a per-bundle basis. So not only can JSON API use _entity_access like the REST module, it can also use _entity_create_access unlike the REST module!
Proposed resolution
Use _entity_access and _entity_create_access, to move access checking for the individual endpoint out of the controller, into the routing system. This benefits performance and scalability.
But … because JSON API has a single "individual" route that does everything: GET, PATCH and DELETE. In other words: there's no way to set _entity_access: node.view for example, because that'd then also be applied for PATCH and DELETE, not just GET!
(Except we can't do GET -> <ENTITY_TYPE.view, because that prevents view label access checking — see #8.)
There are two possible solutions:
- Split up the
individualroute in 3 routes, each with only one HTTP method - We could add a new access check that's like
_entity_access_for_method: 'TRUE', which mapsGETto'view',PATCHto'update'andDELETEto'delete'.
Remaining tasks
TBD
User interface changes
None.
API changes
None.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #32 | interdiff-commit.txt | 767 bytes | gabesullice |
| #29 | 2973784-28.patch | 23.67 KB | gabesullice |
| #28 | interdiff-2973784-26.txt | 1.06 KB | gabesullice |
| #26 | 2973784-26.patch | 22.61 KB | gabesullice |
| #26 | interdiff-2973784-20.txt | 6.17 KB | gabesullice |
Comments
Comment #2
wim leersIt'd look somewhat like this… but it currently can't work!
Because JSON API has a single "individual" route that does everything:
GET,PATCHandDELETE. In other words: there's no way to set_entity_access: node.viewfor example, because that'd then also be applied forPATCHandDELETE, not justGET!There are two possible solutions:
individualroute in 3 routes, each with only one HTTP method_entity_access_for_method: 'TRUE', which mapsGETto'view',PATCHto'update'andDELETEto'delete'.Thoughts?
Comment #3
wim leersForgot patch.
Comment #4
wim leersComment #5
gabesulliceI'd be unopposed to routes per HTTP method.
Comment #6
wim leersCool.
I think this should be done in the 2.x version though, this is absolutely nice-to-have refactoring, with zero practical difference for end users. And little maintainability improvement. It's just the right thing to do, and we get a nice scalability boost.
Comment #7
wim leersPer #5, splitting up the "individual" route in more routes then is a necessary part of the scope of this issue.
Marking NW.
Comment #8
wim leersAFAICT this would interfere with #2843922: Show label of inaccessible entities ('view' access denied) when 'view label' access is allowed: it'd make that downright impossible.
We could still do it for non-
GETroutes though. But it's on theGETroute that we'd get the performance/scalability boost. Therefore, deprioritizing.Comment #9
wim leersReduced scope per #8.
Actually implemented it now. No interdiff, because the previous patch was just a PoC, and it only handled
GET/view, which we no longer can do per #8.Comment #11
wim leersComment #12
wim leersFix mistake in IS introduced in #9.
Comment #13
wim leers#11 fixes
PATCHandDELETEroutes that operate on entities. We still need to handlePOST.That's what this patch does. It uses
_entity_create_access.Comment #14
wim leersClosed #2971652: Separate the collection route from the individual resource creation route as a duplicate, because #13 includes that too.
Comment #16
e0ipsoCode looks good. However it seems that there is a misalignment between the previous error codes and the current error codes. I'm OK with that as long as:
Comment #17
wim leersComment #18
wim leers#16: Great! :)
The patches so far actually don't change any status code at all. The assertion for a 403 response is moved in the test logic to run earlier, because access checking is now happening earlier: it's happening during routing instead of during controller execution. And the response message is now more succinct: rather than , you now just get . That's literally the only observable change :)
Comment #19
gabesullice"view" and "view label" can't be checked in the router, right?
Now that it's more feasible, let's just move this route definition into the "individual" method in
Routes.php. It was odd that it was in the collection method to begin with since it's dealing with individual entities.I think this is where the "create" route belongs.
Comment #20
gabesulliceOh boy, was I ever more wrong? It is not easy to move that to the other section and make the code flow nicely. I hope no one burnt time on that.
This was really confusing to me. I tried to move things around and comment it in a way that seemed clearer.
Otherwise, this looks good to go.
Comment #21
wim leers\Drupal\Core\Entity\EntityAccessCheck(_entity_access),\Drupal\Core\Entity\EntityCreateAccessCheck(_entity_create_access) is what we use here, but\Drupal\Core\Entity\EntityDeleteMultipleAccessCheck(_entity_delete_multiple_access) also exists. So we could add_entity_view_label_or_view_access… but then how would the controller know which of the two applies?So, really, even if we added that (which we shouldn't, core should), then it's not clear how the controller would be informed of which one it is, which is a security risk of itself, which is why I think we won't be seeing this in core.
I had to remove this early return because of that.
The complexity in points 2 and 3 lies in this:
I implemented what you asked in points 2 and 3, but it made this quite a bit more complex.
Comment #22
wim leersOh heh, two competing implementations now :P
I don't have a strong preference for either, I'll let @gabesullice choose. (I completely agree that #20 is the much better, much clearer incarnation of #17!)
Comment #24
wim leersOne more logical change is necessary for the patch in #21. (And a test assertion update.)
Comment #25
gabesulliceIt's nice to see that we essentially ended up in the same place. This is almost exactly what I had. There's one thing that #24 gets wrong though... To be "updatable" or "deletable" it must also be "locatable". That forces one to wrap these in yet another
isLocatable()condition and that makes it messy.I think we should go with something close to #20. I have one final concern about it:
This will probably lead to a major headache for someone trying to do
Url::fromRoute('jsonapi.$resource_type.collection.post')because somewhat mysteriously and apparently randomly, that route will cease to exist.Perhaps we should just fix our tests rather than add this strange logic.
I'll try that.
Comment #26
gabesulliceIMO, this is ready unless tests fail (meaning I probably missed a usage of
Url::fromRoute('jsonapi.$resource_type.collection')).Note that the interdiff is against #20.
Comment #28
gabesulliceTest coverage++
I did miss a special case of
Url::fromRoute('jsonapi.$resource_type.collection')in the entry point. Now that's more robust and won't provide links to collection URLs that don't exist :)Comment #29
gabesulliceDarnit 🤗. Hit save too soon, patch attached.
Comment #30
wim leersWhy not use
collection.post?Other than that one nit, this is RTBC.
Comment #32
gabesulliceFixed the nit on commit, since @Wim Leers said "this is RTBC" and this is blocks #2953346: Define related/relationship routes per field, not dynamically (with route parameters that need validating).