Problem/Motivation
Currently EntityResource
manually checks entity access e.g. in get()
etc. This means these access checks happen during controller execution rather than during routing. In other words: much later, therefore slower, somewhat riskier and "Just Not Right", because we're not using the routing system for everything it can do.
So: why is the rest
module not yet using the _entity_access
/_entity_create_access
route requirement?
As described in #3, this portion of the rest
module actually predates that route requirement's existence! But that doesn't mean we can't start using it now 💪 🙂
Proposed resolution
- Use
_entity_access
route requirement (forGET
,PATCH
andDELETE
routes). Duh, that's the whole point of this issue! 😃 - Don't use
_entity_create_access
, because that requires a bundle to be passed, which is impossible to determine at routing time, since the REST module allows any entity of a given entity type to be sent (of any bundle for that entity type). Confirmed by Entity API maintainer @tstoeckler in #43. - Consequence: a 403 exception (and hence response) can be generated by
\Drupal\Core\Routing\AccessAwareRouter::checkAccess()
. This is long before Dynamic Page Cache comes into play. Which means many 403 responses that were previously cached (since #2765959: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage) are no longer cached. But as the numbers below show, performance is pretty much identical, without any of the I/O — that's a nice scalability win! AccessAwareRouter
in HEAD throwsAccessDeniedHttpException
, i.e. uncacheable 403 exception. We need to make it throwCacheableAccessDeniedHttpException
for cacheable access results to not regress for sites relying on Page Cache/Varnish to reduce the number of 403s being generated in Drupal.- In doing so, certain previously uncacheable 403s now become cacheable. Key example: when REST shipped originally, it first
checked arestful GET entity:node
permission, and then checked$node->access('view', …)
.
Because of that, we need more granular expectations to be definable by the REST tests:EntityResource::getExpectedUnauthorizedAccessCacheability()
is for the generic case (no entity access checked),EntityResource::getExpectedUnauthorizedEntityAccessCacheability()
is the newly added method to define expectations for entity access-related cacheability.
So, a nice win is that REST's 403s are now no longer cached in Dynamic Page Cache, but generated on every request, just much earlier. Performance remains pretty much identical (good), with one crucial difference: no storage space consumed, and no I/O! Performance equal to Dynamic Page Cache's, but none of the costs? Yes, please! Nice scalability win :)
- Patch (uncached 403, generated on every request)
-
Requests per second: 59.70 [#/sec] (mean) Time per request: 16.750 [ms] (mean) Time per request: 16.750 [ms] (mean, across all concurrent requests) Transfer rate: 40.52 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.0 0 0 Processing: 14 17 1.5 16 30 Waiting: 14 17 1.5 16 29 Total: 14 17 1.6 16 30
- HEAD (403 cached by Dynamic Page Cache)
-
Requests per second: 62.02 [#/sec] (mean) Time per request: 16.124 [ms] (mean) Time per request: 16.124 [ms] (mean, across all concurrent requests) Transfer rate: 43.85 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.0 0 0 Processing: 14 16 1.6 16 28 Waiting: 14 16 1.6 16 28 Total: 14 16 1.6 16 28
How we arrived at this proposed resolution
- In the first patches (#5–#15), we're just removing the manual access checking from
EntityResource::get()
etc, and adding the_entity_access
/_entity_create_access
route requirement. - This left us with >200 test failures. Most of those test failures were due to the very detailed REST tests verifying that it's impossible to use authentication mechanisms that aren't explicitly enabled for a given REST resource. @dawehner analyzed it in detail in #21, and posted a solution in #24: a small addition to
\Drupal\Core\EventSubscriber\AuthenticationSubscriber
. - Only ~150 test failures left. Most of them are in the
GET
REST test coverage asserting how responses are processed by Dynamic Page Cache. In #26 and #28, the root cause is explained: access checking is happening earlier now (during routing rather than controller execution), and because Dynamic Page Cache runs fairly early, we need to have core'sRouteAccessResponseSubscriber
run earlier than Dynamic Page Cache's event subscriber. Now that that is fixed, another bug is revealed: Dynamic Page Cache now will no longer cache responses because REST'sResourceRoutes::alter()
sets_csrf_request_header_token
on all routes, includingGET
routes. And for non-mutating requests,\Drupal\Core\Access\CsrfRequestHeaderAccessCheck::access()
allows anything but sets max-age=0, making the response again uncacheable where it wasn't before. Only setting_csrf_request_header_token
on non-GET
routes as suggested >2 years ago in #2737751: Refactor REST routing to address its brittleness solves this! - Down to ~130 failures. All of them for "empty request body" or "unparseable request body" test coverage. Due to changed execution order, the assertions now also need to be moved. See #31 + #32.
- Down to ~110 failures. The remaining ones are due to expected "reason" strings for forbidden access being different than previously expected (more of them now are working as intended!). (#33 + #39)
- Down to ~60 failures. Still more of the same problem … but now only for
POST
requests. A huge problem is that some of them have bundle-dependent logic (e.g.create {bundle} nodes
), but the REST module's route forPOST
ing is something that accepts all bundles of an entity type, hence it's impossible to specify a bundle: -
_entity_create_access: 'node:article'
is impossible, only_entity_create_access: 'node'
is possible! Based on feedback by Entity API maintainer @tstoeckler in #43, this patch stopped using_entity_create_access
in #45 — so access checking is still happening inEntityResource::post()
, but not in any of the other methods. (https://www.drupal.org/project/jsonapi works per bundle, so that module will be able to use this though!) - Back to ~400 failures due to some changes in HEAD. Since #2765959: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage, the REST module has been throwing cacheable 403 exceptions in its controller logic. But since we're moving access checking to routing, we now need to make sure that
AccessAwareRouter
does the same. (#53 + #57 + #58) - Down to ~150 failures. More 403 responses are cacheable now. So we need to change some of the test infrastructure: we need to be able to distinguish between general unauthorized access cacheability (
getExpectedUnauthorizedAccessCacheability()
, already exists) and unauthorized entity access cacheability (new:getExpectedUnauthorizedEntityAccessCacheability()
). (#65 + #67) - Down to ~30 failures. The testing complications with the
ConfigurableLanguage
andConfigurableLanguage
config entity types no longer exists thanks to the clean-up this patch does! (#68) - Down to ~25 failures. More access updates to expected reason for 403s. (#71)
- Green!
Remaining tasks
None.
User interface changes
None.
API changes
Only one API change, and only sort of: a change in a test base class.
- Added
EntityResource::getExpectedUnauthorizedEntityAccessCacheability()
to complementEntityResource::getExpectedUnauthorizedAccessCacheability()
. Necessary for the now more detailed tests.
One behavior change:
AccessAwareRouter
now generates a cacheable 403 exception when the access result is cacheable, rather than always throwing an uncacheable one. This ensures we don't undo the work done by #2920001: Add cacheable HTTP exceptions: Symfony HTTP exceptions + Drupal cacheability metadata + #2765959: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage.
Data model changes
None.
Release notes snippet
The rest.entity.<entity type ID>.GET.<format>
, rest.entity.<entity type ID>.PATCH
and rest.entity.<entity type ID>.DELETE
routes used to have _access: TRUE
as the only route requirement, now they also have the _entity_access: <entity type ID>.view
, _entity_access: <entity type ID>.update
and _entity_access: <entity type ID>.delete
route requirements, respectively. This does not affect runtime code in any negative way; it only makes access checking significantly faster (less expensive) for entity REST resources.
Comment | File | Size | Author |
---|---|---|---|
#114 | 2869426-114.patch | 49.8 KB | Wim Leers |
#109 | 2869426-109.patch | 49.77 KB | Wim Leers |
#109 | interdiff.txt | 1.04 KB | Wim Leers |
#107 | 2869426-107.patch | 48.8 KB | Wim Leers |
#107 | interdiff.txt | 2.73 KB | Wim Leers |
Comments
Comment #2
Wim LeersI remember articulating exactly this concern/complaint, trying to make it work, and then there being a good reason not to have that. Let me dig that up.
Comment #3
Wim LeersThe design for the current approach was introduced in #1866908: Honor entity and field access control in REST Services, then the missing stuff was added in #1947432: Add a generic EntityAccessCheck to replace entity_page_access(), and then eventually I worked to get it cleaned up in #2664780: Remove REST's resource- and verb-specific permissions for EntityResource, but provide BC and document why it's necessary for other resources.
Specifically see #2664780-28: Remove REST's resource- and verb-specific permissions for EntityResource, but provide BC and document why it's necessary for other resources, and this particular portion of it:
Then see #46, #47, #48.
However… if you see a way to make it work though, be my guest! I'd love for things to work this way! It's more feasible to introduce that now, because we now have solid test coverage, we had close to zero REST test coverage back then.
But, given that things are working correctly, this would qualify as a minor task.
Comment #4
tstoecklerSo we have
_entity_create_access
for that. I didn't play with it yet, though, so maybe there are other problems.Comment #5
dawehnerYou know, let's try it out :)
Comment #6
Wim LeersYou beat me to it :)
Comment #8
tstoecklerYou missed the "_access" suffix of the requirement keys, if I'm not mistaken ;-)
Looks sweet otherwise!
Comment #9
dawehnerDetails seem to matter ...
Comment #10
dawehnerComment #13
Wim LeersPatch review:
#9 didn't update this one yet.
The failing tests show this:
IOW: this change does seem to work, but it seems to prevent
\Drupal\Core\EventSubscriber\AuthenticationSubscriber::onKernelRequestFilterProvider()
from firing as before. This sounds like a bug in therequest processing system
…Comment #14
Wim LeersPatch no longer applied. Straight reroll.
Comment #15
Wim LeersFixed #13's first remark.
This should reduce the number of failures.
Comment #18
Wim LeersBecause testbot only reports number of test classes failed and not number of test methods failed, #15 doesn't seem to fix anything. But it really does reduce the number of fails.
#14:
vs #15:
Comment #19
Wim LeersStrangely, testbot didn't mark this NW.
Comment #20
dawehnerLet me have a look at the remaining failures
Comment #21
dawehnerHere is a quick anaylsis of this problem:
Before the flow looked like the following:
Afterwards the flow is the following:
Given that the entity access checking will always be executed before the authentication subscriber and as such its impossible to produce the authentication checking. I'm wondering though whether we could call the logic in
\Drupal\Core\EventSubscriber\AuthenticationSubscriber::onKernelRequestFilterProvider
in an exception subscriber as well?Comment #22
Wim LeersGreat analysis! Thanks @dawehner :) 👍 Totally makes sense now!
I also like the sound of your proposed solution, I'm curious to see what the code would look like.
Comment #23
Wim LeersComment #24
dawehnerLet's give it a try
Comment #26
Wim LeersNice, from 216 fails to only 132!
I took a look at one of the more frequent remaining test failures. It's in the
GET
test coverage for Dynamic Page Cache that is breaking. But only for HAL+JSON responses. The patch appears to somehow prevent theurl.site
cache context from being associated with the response, which means there's no cache redirect in the response cached in Dynamic Page Cache, which causes the failure.Root cause: not yet determined, but likely the different execution order.
Note that #2864816: HAL LinkManager doesn't add 'url.site' cache context when needed could fix this.
Comment #27
Wim Leers#2864816: HAL LinkManager doesn't add 'url.site' cache context when needed just landed.
Let's reroll this.#24 still applies. Retesting it. It was at 132 fails before retesting. Let's see…Comment #28
Wim LeersSo now it's at 148. (Probably also because more REST tests were committed in the mean time.)
In any case, #2864816: HAL LinkManager doesn't add 'url.site' cache context when needed didn't help.
I did some actual debugging this time. In HEAD, for
Editor
config entity REST responses, there is theuser.permissions
cache context. With this patch, that's no longer the case.The root cause for this is that the access checking is moved out of the controller, into the routing system. The routing system's access result is stored in
$request->attributes[AccessAwareRouterInterface::ACCESS_RESULT]
. But it's not being merged back into the response object before Dynamic Page Cache processes it, becauseRouteAccessResponseSubscriber
runs after Dynamic Page Cache'sKernelEvents::RESPONSE
subscriber. That's easy enough to fix.But then we encounter another problem: the access result is uncacheable, because
\Drupal\Core\Access\CsrfRequestHeaderAccessCheck::access()
runs for every single REST route match. This is one of those long-standing bugs in the REST module's routing architecture. I highlighted it in #2737751: Refactor REST routing to address its brittleness about 1.5 year ago:Well, I was overly optimistic: it is harmful. That was demonstrated here.
So this patch makes two changes: updates the priority of
RouteAccessResponseSubscriber
, and changesResourceRoutes
to only set the_csrf_request_header_token
route requirement when actually necessary.Now lots of tests should start passing :)
Comment #29
BerdirI could be wrong, but I remember that we once discussed that it is by design that route access cacheability is *not* considered by dynamic page cache because dynamic page cache runs *after* route access checking.
But maybe I misunderstood something here.
Comment #31
Wim LeersMany of those failures are of the kind
Failed asserting that 403 is identical to 400.
— those occur because of the reason given by @dawehner in #21. Those checks now happen after logging in. That's a soft behavior change (only in case of invalid requests). Essentially, this changes the "request validation order". So just moving some of the existing checks until after the request contains authentication and until the user is authorized to perform the operation, is enough to fix those test failures.Comment #32
Wim Leers#31 updated
testPatch()
. I should also have updatedtestPost()
.Comment #33
Wim LeersIn 42 cases, the
GET
orDELETE
test is failing on asserting the expected error message in case of a 403: the expected message is some string, the actual one … is empty. In all of these cases, it's the fallback message generated byEntityResource::generateFallbackAccessDeniedMessage()
, which is what was used in case the access result didn't contain a reason.This can and should be done in a separate issue, on which this issue should be blocked. For now, doing it here, so we can prove it works. Attached is a patch that fixes it for
Media
andUser
(which should fix 12 of the failures, but not all).Comment #37
Wim LeersThis change in #28 was mostly right, but not entirely right. I forgot about
DELETE
, which is why there are now 30 test failures like this:Comment #39
Wim LeersThe good
Continuing #33: fixed
Block
+Comment
+ConfigTest
+Media
(#33 only took care of thedelete
operation, this does the same for theupdate
operation) +MenuLinkContent
+ShortcutSet
+User
.The Bad
There were some I could not fix easily:
EntityTestLabel
+EntityTest
+Node
+Shortcut
+Term
. What's special about these?All of them have access logic that is bundle-dependent. And apparently the
_entity_access
access check is nothave the same root cause
$entity_bundle
in the create access check is missing. Because this patch does:But that isn't yet setting the bundle type like
\Drupal\Core\Entity\EntityCreateAccessCheck::access()
expects not justnode
, butnode:{node_type}
, orshortcut:{shortcut_set}
. Where that latter portion is then referring to the bundle type that should be passed to the create access check, and is in fact a placeholder for a request argument.The Ugly
We can fix that route requirement.
But it still will fail. Because REST's POST routes are not bundle-specific. Which means the only options we have are:
_entity_create_access
\Drupal\Core\Entity\EntityCreateAccessCheck::access()
to somehow receive the bundle from the request body…?To understand this, see the following hunks:
The only failures now should be of the kind "Could not find {blah} request argument…"
Comment #41
Wim LeersPinged @dawehner on IRC and @tstoeckler on Twitter: https://twitter.com/wimleers/status/953325138312908801.
Comment #43
tstoecklerYeah, I think you are spot on in #39. Since I think using
_entity_access
is already a nice improvement in itself, I would vote for punting on_entity_create_access
for now. The entity-bundle thing really is pretty weird when you are coming from a pure Rest point of view, at least with the choice of the core Rest module to declare entity type === resource. Is it true that JSON API and GraphQL have actually chosen per-bundle resources instead of per-entity-type resources? Maybe they can at least use_entity_create_access
then... ?!Comment #44
Wim LeersI don't know about GraphQL, but yes, JSON API has chosen that. So, yes, JSON API should be able to use that :)
Alright, rerolling this patch to use
_entity_access
for all cases exceptPOST
then!First, rebasing on top of HEAD. Sadly, #2862422: Add per-media type creation permissions for media made Media's access control handler worse than it was before. I just dropped all changes for it.
Comment #45
Wim LeersPer #44, reverted the
_entity_access_create
-related bits. This should pass tests, because it was the sole reason some tests failed to pass in #39. Of course, things could have changed in the mean time…Comment #48
tstoecklerThanks!! Let's get this done.
I have some remarks, nothing major though, generally looks quite good!
As far as I can tell this is modelled after
AuthenticationSubscriber::onKernelRequestFilterProvider()
. I think we should add theisset($this->filter)
check to the outer if here, as well.Would be nice to pass
$exception
as$previous
into the constructor of the new exception.Should this be split up to use e.g.
allowedIfHasPermission()
,andIf()
, etc.Shouldn't this mention that the user must the comment author?
Unused.
Looks like leftover debug?
Maybe:
would be a bit more concise?
Same for
ShortcutSetResourceTestBase
Comment #49
dawehnerNice catch!
👍
After changing it, this got way more readable.
Sure, why not
I went with not changing this because I thought these might need additional steps once we add POST/UPDATE support.
This doesn't yet resolve all the test failures of the rest tests.
Comment #51
tstoecklerAwesome. I think you can drop the
cachePerPermissions()
, actually, asallowedIfHasPeemissions()
already does that.Sure, makes sense.
Comment #52
Wim LeersThis can be removed,@tstoeckler already said that in #51.\Drupal\Core\Access\allowedIfHasPermission()
already does this for us :)Also, a small bug snuck in there, causing the number of test failures to go up.
❤️
Comment #53
Wim LeersI investigated the remaining failures. Most of them are due to a 4xx response not containing
X-Drupal-Cache-Tags
,X-Drupal-Cache-Contexts
,X-Drupal-Dynamic-Cache
andX-Drupal-Cache
headers. IOW: the responses are not cacheable, and are no longer cached by Dynamic Page Cache nor Page Cache!Why is that?
Well, first: why are those 4xx responses even cacheable? Because not that long ago we made them cacheable, in #2765959: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage. The entire reason they are cacheable is because we changed the
throw new AccessDeniedHttpException(…)
tothrow new CacheableAccessDeniedHttpException($entity_access, …)
in\Drupal\rest\Plugin\rest\resource\EntityResource::get()
.But this patch is removing the access checking from
\Drupal\rest\Plugin\rest\resource\EntityResource::get()
, and is moving that into the routing system. So now it's\Drupal\Core\Routing\AccessAwareRouter::checkAccess()
that is throwingthrow new AccessDeniedHttpException()
.We can change that to
throw new CacheableAccessDeniedHttpException($access_result, …)
in the appropriate circumstances.Once that's done, the tags and contexts assertions do pass. But the Dynamic Page Cache assertion does not yet pass: the 4xx responses are no longer cached by Dynamic Page Cache. (They now are cached by Page Cache!)
Why?
Because of this in
\Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber::onResponse()
:So is there harm in losing Dynamic Page Cache support? Or do we win more by having access checking happening at the routing level, i.e. long before a controller is even instantiated? Benchmarking has the answer:
Pretty much identical performance to having the responses served from Dynamic Page Cache. With one crucial difference: no storage space consumed, no I/O!
Performance equal to Dynamic Page Cache's, but none of the costs? Yes, please! That's a nice scalability win :)
Comment #54
Wim LeersTo clarify the patch in #53, here are some comments that should help:
This is the change explained at length in the previous comment.
This is changing from "cached by DPC" to "not cached by DPC".
This is changing from "not cacheable at all" to "cacheable", by Page Cache.
(This is the test coverage for D8.0/8.1 sites that use the
restful get entity:node
-style permissions. Those always were checked at the route level, but never were cacheable, until today!)Comment #57
Wim LeersMissed two occurrences of the thing mentioned in #54.3.
Comment #58
Wim LeersSome more 403 assertions that were not cacheable, and now are. Yay for strict testing.
Comment #61
Wim LeersThis should fix most of the
testPatch()
andtestDelete()
failures.Comment #62
Wim LeersAnd this should fix all the
testPost()
failures.This reverts #32, which made changes per the analysis that @dawehner made in #21. That analysis is still accurate, but no longer applies to
POST
, because we concluded in #39 that the REST module cannot use_entity_create_access
, hence we stopped using it, and hence the changes totestPost()
are no longer necessary.Comment #65
Wim LeersThe high-level explanation for most of the test failures is that the cache tags of the 403 responses are now different. Because
\Drupal\Core\Access\AccessManager::check()
does:And it then proceeds to check each of these in the order given. And that order happens to be:
Which means the entity access check happens after the
restful get entity:node
permission check. This is also the fastest (entity access checks are inherently slower than permission access checks).Great.
So, updated the existing
getExpectedUnauthorizedAccessCacheability()
code to match the new (improved!) behavior. Special care was necessary forBlock
entities, because #2820315: Blocks' 'view' operation is interpreted as "render on page" instead of "read this config entity", which has been a known issue for a very long time.This took many hours to figure out, so glad this is done! Hopefully I didn't miss any more edge cases… (I'm relieved this at least this required only test coverage changes, and no changes in the request processing system!)
Comment #67
Wim LeersYay, from 87 to 54!
This set of next failures is simpler to fix, fortunately.
Comment #68
Wim LeersThis round is even simpler: the
BasicAuthResourceWithInterfaceTranslationTestTrait
alternative toBasicAuthResourceTestTrait
that was added in #2765959: Make 4xx REST responses cacheable by (Dynamic) Page Cache + comprehensive cacheability test coverage to accommodate theConfigurableLanguage
andContentLanguageSettings
config entity types' testing is no longer necessary! 🎉Comment #71
Wim LeersThe failures in
Comment
+File
+Media
are related to changes we were making in earlier patch iterations to their access control handlers. But rather than doing that here, I think we should improve those in a follow-up issue, and just accept that right now, they go from a somewhat-but-not-truly-helpful default fallback message as the reason for access not being granted, to no message at all. Otherwise, we make this patch even harder to review!I already lost the
MediaAccessControlHandler
changes in the rebase last week (solving the conflict was far from trivial), and in this patch I'm explicitly reverting the changes toCommentAccessControlHandler
.Created 3 Novice follow-up issues:
Comment #72
Wim LeersWith some luck, #71 will be green! 😮
This issue really needs an updated summary though!
Comment #73
Wim LeersIT IS GREEN! 🍻
Comment #74
borisson_Why are we not using
$access_result->isForbidden
instead of!$access_results->isAllowed
?Comment #75
Wim LeersWe could do that in this one particular case, but in general using
isForbidden()
is a bad practice. Because it could also beisNeutral()
! Checking!isAllowed()
covers both. Both are a way of disallowing access. One is still overridable (isNeutral()
), one is final (isForbidden()
).Comment #76
borisson_Awesome, thanks for that answer @Wim Leers! That makes sense. That was the only thing I could find. This issue still needs a summary update.
Comment #77
Wim LeersCool!
Great coincidence — I had begun working on doing exactly that! Will post it later today — hopefully you'll be able to RTBC then :)
Comment #78
Wim LeersStill working on that IS update, but this patch badly needs to be rebased too. It was green in #71, 2 months ago. It no longer applies.
Conflicts with at least the following:
Fixed all conflicts, hopefully didn't make too many mistakes.
Comment #79
Wim LeersComment is not yet updated.
Nit: Unnecessary change, reverted.
Comment #80
Wim LeersThis was very hard to write. It took … much longer than I would have liked (HOURS!). I'm sure it can be better still, but this should still be a huge step forward. Hopefully it's good enough.
Comment #81
Wim LeersOne typo in HTML and it looks like 💩
Comment #82
Wim LeersThis can be moved to a separate issue. So did that: #2973347: EntityCreateAccessCheck should provide a useful error message when no bundle is specified.
Yay for slightly smaller patch!
Comment #83
Wim LeersWhile updating the IS for #80, I also noticed nobody ever replied to @Berdir's review/question in #29. Sorry, @Berdir! 😟
Of course, @Berdir being @Berdir (is that enough @Berdir for you in less than 3 sentences? 😀), he asks hard yet excellent questions. I even went back to the issue that introduced Dynamic Page Cache (#2429617: Make D8 2x as fast: Dynamic Page Cache: context-dependent page caching (for *all* users!)) to check what the outcome of the discussion there was.
#29:
To my surprise 😲 and fear 😱, this was NEVER DISCUSSED! Zero mentions of #2429617-219: Make D8 2x as fast: Dynamic Page Cache: context-dependent page caching (for *all* users!) mentions it. But doesn't dive in deeper. So I instead looked at every occurrence of . Again nothing mentioned.
.But I think @Berdir is right. It's just unfortunate that despite Dynamic Page Cache having such detailed test coverage, this is not something that is explicitly tested. (If it were, then the change made to
\Drupal\Core\EventSubscriber\RouteAccessResponseSubscriber::getSubscribedEvents()
should trigger a test failure.)And I think the reason this is only coming up now and not months or years ago, is that the REST module's responses are atypical. Unlike pretty much all other responses, the controller in question (
\Drupal\rest\Plugin\rest\resource\EntityResource::get()
) has been:(The closest analogies are block and entity rendering, but those are not route/response-level, they're intra-response-level.)
So, given that REST was doing something exceptional and this is a change that merits broader discussion … let's revert those changes. And let's instead let
EntityResource::get()
continue to do what it does today wrt cacheability, but not wrt access checking. This minimizes change.Created #2973356: Evaluate making DynamicPageCacheSubscriber::onResponse() run after RouteAccessResponseSubscriber::onRespond(), to take into account route access cacheability for evaluating this.
(IS already up-to-date, I wrote it with this in mind.)
Comment #84
BerdirHa, that's a very dramatic comment ;)
I'll comment over there.
Comment #85
borisson_The updated IS makes this a lot easier to grok. Great work @Wim Leers!
Comment #86
Wim LeersThanks @borisson_!
Nice: #2973347: EntityCreateAccessCheck should provide a useful error message when no bundle is specified already landed, meaning that nobody else in >=8.6 should need to debug for a long while to figure out why/how
_entity_create_access
is not working for them :)Comment #87
Wim LeersOpened a sibling issue for JSON API: #2973784: JSON API should check entity access during routing, not in controller, for the "individual" route.
Comment #89
tacituseu CreditAttribution: tacituseu commentedUnrelated failure.
Comment #91
alexpottAre we sure we want two events with exactly the same weight? I think that needs a comment as to why. Also I think onExceptionSendChallenge should always come before onExceptionAccessDenied - so can we make the new listener on 80 or thereabouts?
The comment is being made out-of-date by the patch.
Why are we making this change? Ah I see this is described in the issue summary. It feels like a comment belongs here. Also looking at \Drupal\Core\Access\CsrfRequestHeaderAccessCheck::applies() it looks like this list should be
['GET', 'HEAD', 'OPTIONS', 'TRACE']
. I wish we had a version of \Symfony\Component\HttpFoundation\Request::isMethodSafe(), ::isMethodIdempotent(), ::isMethodCacheable() where we didn't need a request first.Comment #92
Wim Leers\Drupal\Core\Access\CsrfRequestHeaderAccessCheck::access()
to return a cacheableAccessResult::allowed()
when just the HTTP method already shows CSRF checking is unnecessary. It's only when the user is authenticated and there is a session cookie on the request that we needmax-age = 0
.Comment #93
dawehnerLet's use a strict in_array here, you never know :(
This bit of the comment seems to no longer apply, maybe need to move this documentation up.
I tried to write a test in the routing tests for the added 403 cacheability in
core/tests/Drupal/KernelTests/Core/Routing/ExceptionHandlingTest.php:49
. Sadly\Drupal\Core\PageCache\RequestPolicy\CommandLineOrUnsafeMethod
makes that impossible right now. I am wondering though whether it is "enough" to test it just in REST itself.@Wim Leers Maybe you have an opinion about it?
Comment #95
Wim Leers#93:
Rebased. Zero changes compared to #93.
Comment #96
Wim LeersWhat about this? :)
Comment #100
dawehner@Wim Leers
Good idea!
Comment #101
alexpottThis change is surprising and I think needs a comment
Let's deprecate this rather than delete - something in contrib or custom could be using it - no? Yeah this went into 8.5.0 so we have to remove it more carefully.
Comment #102
Wim Leers#101
(Because access checking is now happening earlier.)
IOW: only
EntityResourceTestBase
subclasses that installed thelanguage
module needed it. That does mean that in hindsight,BasicAuthResourceWithInterfaceTranslationTestTrait
should have been marked@internal
. But breaking risk is extremely small. Which entity types require thelanguage
module to installed?Comment #104
Wim LeersRandom (infra?) fail in
Drupal\Tests\quickedit\FunctionalJavascript\QuickEditIntegrationTest
.Comment #107
Wim Leers#2976334: Allow Custom blocks to be set as non-reusable adding access restriction based on where it was used. caused this to fail tests.
Also, this is now against 8.7, and 8.7 changed some things. So this patch needs to adapt (some changes introduced in #65 need to be reverted + use
->isMasterRequest()
now). Interdiff provided.Hoping this passes tests again! 🤞
Comment #109
Wim Leers#2981887: Add a publishing status to taxonomy terms also caused this to fail tests, but that didn't show up yet in #102, because it was committed a day later.
Comment #110
larowlanSo what happens if someone has extended from this controller. I realise controllers aren't included in the API. But in theory someone might do it. They would also need to update their routing to avoid a security issue?
i think we need to wrap here, can be fixed on commit
Re #1 - i guess we hope they also call this parent method?
do we need to keep this around and deprecate it for BC?
note to self, not an api break because 1:1 rule
nice
Comment #111
Wim LeersEntityResource::get()
) is not a controller, but a@RestResource
plugin.\Drupal\rest\RequestHandler::handle()
is a controller. That's the controller that eventually invokes a@RestResource
plugin method (get()
,post()
,patch()
ordelete()
).It is somewhat conceivable that somebody would provide a different implementation for a particular entity type. They'd need to implement
hook_rest_resource_alter()
and override the class for the@RestResource
plugin derivative for that particular entity type. Like the example inrest.api.php
shows, something like:Such as class may very well override the
get()
method of the parent class. But if they do that, then the route definitions would still get the_entity_access
route requirements. And even if they go so far as overridinggetBaseRoute()
(where that route requirement is added), they'd still be wanting to callparent::getBaseRoute()
first. If they don't, they would already be opting out of everything that the REST module provides for them.In other words: overrides are unlikely, but if they happen, then unless it's implemented extremely poorly, they'll still get the same access control as is being specified in this patch!
Comment #112
alexpott<3 AccessResultReasonInterface FTW
Comment #113
alexpottAdding issue credit for @larowlan, @tstoeckler, @Berdir and myself for comments that influenced the patch.
Comment #114
Wim LeersYAY! :D
Reroll entirely automatic thanks to
git
❤️qComment #115
alexpottI think we should have a change record and a release notes section to detail the change here - ie. that rest routes now have the _entity_access requirement automatically added and that this should not affect any runtime code in a negative way.
Also the issue summary says there are two behaviour changes and then proceeds to list one. I'm guessing that the missing one is the _entity_access route requirement - but that is just a guess.
Comment #116
Wim LeersGood points. Done!
<ol>
in the issue summary.Comment #117
Wim LeersHTML mistake 😭
Comment #118
alexpottCommitted 701be77 and pushed to 8.7.x. Thanks!
Comment #121
xjmSo the release note and CR here don't mention any disruptive changes; does this belong in the release notes? As a reminder release notes should only be for things that might have some disruptive impact on existing modules or sites.
The only possible disruption I'm getting from the IS is:
AccessAwareRouter now generates a cacheable 403 exception when the access result is cacheable, rather than always throwing an uncacheable one.
...which is only disruptive if your cache invalidation is wrong, I think.
So this probably should be 8.7 highlights instead I think!