Problem/Motivation

Facts for Drupal 8.0 & Drupal 8.1's REST:

  1. REST module's EntityResource respects Entity Access & Field Access.
  2. REST module requires every entity type to be explicitly enabled in rest.settings.yml.

Therefore it is very confusing that every entity type gets another permission, per verb, to be able to actually use that REST resource.

Proposed resolution

@klausi at #1816354-1: Add a REST module, starting with DELETE:

Access control is also a problem right now, we don't have that in the routing system yet (see #1793520: Add access control mechanism for new router system for work on that). And we don't have something like entity_access() or access controllers in core (see #1696660: Add an entity access API for single entity access for that). I think I will just expose my own permissions right now ("read resourceXY over REST API", "create resourceXY over REST API" etc.) and check for that.

That is the issue that brought REST to core. It was not at all discussed further. No follow-up was created. In other words: very little scrutiny was applied to the REST module before it could be committed to Drupal 8. I'm certain such a patch would not have been able to land in the past few years (say, since 2014) — it looks like we were far more lax in 2012.

It's totally possible that access handling in general and permission handling in particular were discussed elsewhere, but if so, I could not find it (I looked at all these issues).

The only other issue that was remotely related is #1866908: Honor entity and field access control in REST Services. In there, two commits happened:
the first marked \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()'s permissions as "restricted" (this method no longer exists), as a stopgap. The second commit removed that, and instead added proper entity access checking. But … it did in fact NOT remove the permissions for entities! It just kept them!

In other words, the only reason permissions still exist, at least for EntityResource, is simply that this was overlooked. No follow-up issue was created, and thus it was simply forgotten. It makes sense to allow @RestResource to opt in to having a permission, for example in the case of DbLogResource. But it does not make sense to always have this, without even the ability to opt out!

We (which includes dawehner, Crell, I, and others) also discussed this at DrupalCon New Orleans and unanimously agreed that:

  1. it does not make sense for Drupal 8 to continue to have permissions enabled by default for EntityResource
  2. we should not break BC
  3. we should therefore make sure that newly installed Drupal 8.2 sites don't need to enable these permissions for EntityResource, but existing Drupal 8 sites will continue to use that

This issue/patch therefore proposes:

  1. we keep permissions enabled by default for @RestResource plugins: this guarantees BC for all custom/contrib REST resources
  2. we remove permissions from EntityResource in Drupal 8.2.x by default, but keep them enabled for existing Drupal installations
  3. we document A) why we have permissions, B) how you can opt out from permissions for a particular REST resource

Remaining tasks

None.

User interface changes

None.

API changes

  • The EntityResource @RestResource opts out from generating the permissions that ResourceBase generates by default, but only for new sites.
  • Other @RestResources continue to generate permissions. This means all contrib/custom ones continue to work as they do today.
  • For details, see the change record: https://www.drupal.org/node/2733435.

Data model changes

New key in rest.settings configuration: bc_entity_resource_permissions. Upgrade path provided: rest_update_8203().

CommentFileSizeAuthor
#79 interdiff.txt1.05 KBWim Leers
#79 rest_permissions-2664780-79.patch32.88 KBWim Leers
#77 rest_permissions-2664780-77.patch32.15 KBWim Leers
#72 2752325-72.patch27.54 KBtedbow
#67 rest_permissions-2664780-67.patch32.88 KBWim Leers
#67 interdiff.txt2.25 KBWim Leers
#66 interdiff.txt1.84 KBWim Leers
#66 rest_permissions-2664780-66.patch32.79 KBWim Leers
#59 interdiff.txt9.53 KBWim Leers
#59 rest_permissions-2664780-59.patch37.72 KBWim Leers
#55 interdiff.txt6.67 KBWim Leers
#55 rest_permissions-2664780-55.patch29.5 KBWim Leers
#54 interdiff.txt788 bytesWim Leers
#54 rest_permissions-2664780-54.patch24.59 KBWim Leers
#45 interdiff.txt1.59 KBWim Leers
#45 rest_permissions-2664780-45.patch24.13 KBWim Leers
#43 rest_permissions-2664780-43.patch22.57 KBWim Leers
#41 interdiff.txt2.7 KBWim Leers
#41 rest_permissions-2664780-41.patch22.53 KBWim Leers
#34 interdiff.txt4.91 KBWim Leers
#34 rest_permissions-2664780-34.patch21.7 KBWim Leers
#31 interdiff.txt1.22 KBWim Leers
#31 rest_permissions-2664780-31.patch17.35 KBWim Leers
#29 interdiff-3.txt1.46 KBWim Leers
#28 rest_permissions-2664780-28-3.patch16.92 KBWim Leers
#28 interdiff-2-b.txt813 bytesWim Leers
#28 rest_permissions-2664780-28-2-b.patch16.93 KBWim Leers
#28 interdiff-2-a.txt2.58 KBWim Leers
#28 rest_permissions-2664780-28-2-a.patch17.82 KBWim Leers
#26 interdiff.txt752 bytesWim Leers
#26 rest_permissions-2664780-26.patch16.16 KBWim Leers
#24 interdiff.txt8.05 KBWim Leers
#24 rest_permissions-2664780-23.patch15.46 KBWim Leers
#22 interdiff.txt1.15 KBWim Leers
#22 rest_permissions-2664780-22.patch9.01 KBWim Leers
#20 interdiff.txt3.13 KBWim Leers
#20 rest_permissions-2664780-20.patch8.58 KBWim Leers
#11 rest_permissions-2664780-11.patch6.57 KBWim Leers
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers created an issue. See original summary.

Crell’s picture

Because not all resources are entities. In practice the bulk of them are, but REST module supports any arbitrary resource model you want to throw at it. Entities are the special case in that they have an extra access control layer of their own. But even then, you'd get a 403 error when you really want a 405 error.

Wim Leers’s picture

So then why don't we only add permissions for non-entity resources?

I.e. why don't we allow the @RestResource annotation to opt out by specifying needs_permissions = FALSE?

dawehner’s picture

Well, its a good question whether we want all users which can edit any kind of content to also change things via a REST api. Just imagine Drupal.org, do you want to update content using some REST api vs. on the website?

Wim Leers’s picture

But you first have to expose a certain REST resource in the first place. Per verb even.

On top of that, you must also use the authentication mechanism specified.

klausi’s picture

different permissions per verb: you want to grant GET access to all users, but you only want POST access for a special group of API users. Users can still create nodes via HTML forms, but you might not want to allow that over the REST API - what dawehner said.

Wim Leers’s picture

But isn't that an edge case that should be addressed by contrib? Right now, actually doing a single REST request requires so much non-obvious, Drupal-y steps.

If you only want to grant POST access to a special group of API users… then implement entity access hooks! That's what they're here for.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

dawehner’s picture

So should we create a subissues just for entities to turn it off for them?

Wim Leers’s picture

Sounds good!

Wim Leers’s picture

Title: Remove REST's resource-and-verb-specific permissions or document why it's necessary » Remove REST's resource-and-verb-specific permissions for EntityResource, but provide BC, document why it's necessary for other resources
Priority: Normal » Major
Status: Active » Needs review
Related issues: +#1816354: Add a REST module, starting with DELETE, +#1866908: Honor entity and field access control in REST Services
FileSize
6.57 KB

Wow, look what I just found: @klausi at #1816354-1: Add a REST module, starting with DELETE:

Access control is also a problem right now, we don't have that in the routing system yet (see #1793520: Add access control mechanism for new router system for work on that). And we don't have something like entity_access() or access controllers in core (see #1696660: Add an entity access API for single entity access for that). I think I will just expose my own permissions right now ("read resourceXY over REST API", "create resourceXY over REST API" etc.) and check for that.

That is the issue that brought REST to core. It was not at all discussed further. No follow-up was created. In other words: very little scrutiny was applied to the REST module before it could be committed to Drupal 8. I'm certain such a patch would not have been able to land in the past few years (say, since 2014) — it looks like we were far more lax in 2012.

It's totally possible that access handling in general and permission handling in particular were discussed elsewhere, but if so, I could not find it (I looked at all these issues).

The only other issue that was remotely related is #1866908: Honor entity and field access control in REST Services. In there, two commits happened:
the first marked \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()'s permissions as "restricted" (this method no longer exists), as a stopgap. The second commit removed that, and instead added proper entity access checking. But … it did in fact NOT remove the permissions for entities! It just kept them!

In other words, the only reason permissions still exist, at least for EntityResource, is simply that this was overlooked. No follow-up issue was created, and thus it was simply forgotten. It makes sense to allow @RestResource to opt in to having a permission, for example in the case of DbLogResource. But it does not make sense to always have this, without even the ability to opt out!


We (which includes dawehner, Crell, I, and others) also discussed this at DrupalCon New Orleans and unanimously agreed that:

  1. it does not make sense for Drupal 8 to continue to have permissions enabled by default for EntityResource
  2. we should not break BC
  3. we should therefore make sure that newly installed Drupal 8.2 sites don't need to enable these permissions for EntityResource, but existing Drupal 8 sites will continue to use that

I therefore propose that :

  1. we keep permissions enabled by default for @RestResource plugins: this guarantees BC for all custom/contrib REST resources
  2. we remove permissions from EntityResource in Drupal 8.2.x by default, but keep them enabled for existing Drupal installations
  3. we document A) why we have permissions, B) how you can opt out from permissions for a particular REST resource

Attached patch does all this.

dawehner’s picture

we remove permissions from EntityResource in Drupal 8.2.x by default, but keep them enabled for existing Drupal installations

Nice!

  1. +++ b/core/modules/rest/rest.install
    @@ -21,3 +21,13 @@ function rest_requirements($phase) {
    +/**
    + * Enable BC for EntityResource: continue to use permissions.
    + */
    +function rest_update_8201() {
    +  $config_factory = \Drupal::configFactory();
    +  $rest_settings = $config_factory->getEditable('rest.settings');
    +  $rest_settings->set('bc_entity_resource_permissions', TRUE)
    +    ->save(TRUE);
    +}
    

    I'm wondering whether this could be also a post update ... I guess it doesn't really make a difference.

  2. +++ b/core/modules/rest/src/Plugin/ResourceBase.php
    @@ -191,14 +196,22 @@ public function availableMethods() {
    +    $requirements = array();
    +
    +    // Only specify route requirements if the default permission exists. For any
    +    // more advanced route definition, resource plugins extending this base
    +    // class must override this method.
    +    $permission = "restful $lower_method $this->pluginId";
    +    if (isset($this->permissions()[$permission])) {
    +      $requirements['_permission'] = $permission;
    +    }
    

    Its so weird that permissions() returns always all of them, but really not the problem of this issue.

  3. +++ b/core/modules/rest/src/Tests/RESTTestBase.php
    @@ -60,6 +60,12 @@ protected function setUp() {
    +
    +    // @todo Remove this, and test the BC separately?
    +    $config_factory = \Drupal::configFactory();
    +    $rest_settings = $config_factory->getEditable('rest.settings');
    +    $rest_settings->set('bc_entity_resource_permissions', TRUE)
    +      ->save(TRUE);
    

    Well, we at least need some update test, don't we?

klausi’s picture

Wim +1. Once you enable a specific entity:* resource in rest.settings.yml you already want it to work because we have custom entity and field access handling.

I think the permissions are important for other resources, and the issue title tells me we are addressing this :)

It is a bit sad that we cannot remove the permissions for EntityResource completely, or can we? How can a site break if a permission is just gone?

Wim Leers’s picture

How can a site break if a permission is just gone?

They may start to expose data that wasn't exposed before.

dawehner’s picture

They may start to expose data that wasn't exposed before.

On one hand this is true, but on the other hand, these entities could be available via the normal HTML based interface already. In general though I agree, there is some level of risk involved here.

Wim Leers’s picture

Yep, there is some risk, and having this BC flag set by default for existing sites means we avoid all that risk.

klausi’s picture

Right, it could be a security risk if people do custom stuff with EntityResource.

Can we avoid using \Drupal:: in ResourceBase? But changing the constructor of a plugin base class is probably a BC break :-(
Accessing config with \Drupal should at least go into its own method getRestSettings() or similar.

We could create a new base class BetterResourceBase or EightTwoResourceBase, which is also weird. And that's why you should not write base classes for plugins and always use traits instead. ResourceTrait would have been the way to go.

klausi’s picture

Status: Needs review » Needs work

Oh forget what I said, the change is in EntityResource anyway. We can override the constructor there and don't need to use \Drupal.

The last submitted patch, 11: rest_permissions-2664780-11.patch, failed testing.

Wim Leers’s picture

Assigned: Unassigned » Wim Leers
Status: Needs work » Needs review
Issue tags: +Needs upgrade path tests
FileSize
8.58 KB
3.13 KB

#12:

  1. Happy to change it if need be, keeping as-is for now since you say this is fine.
  2. Indeed somewhat strange. I mostly just find it strange that this lives on ResourceInterface.
  3. Right, that makes much more sense than that comment :P

#17:

We could create a new base class BetterResourceBase or EightTwoResourceBase, which is also weird. And that's why you should not write base classes for plugins and always use traits instead. ResourceTrait would have been the way to go.

Indeed!

#18: also true. Done!


Next reroll will address #12.3.

Status: Needs review » Needs work

The last submitted patch, 20: rest_permissions-2664780-20.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
9.01 KB
1.15 KB

Wow, this took ages to debug. The reason for the test failures in PageCacheTest::testQueryParameterFormatRequests()… is that the current patch allows the routes for EntityResource to not have any requirements. And without requirements, no access checks. Without access checks, our routing system assumes you forgot to specify access checks, and hence helpfully denies access. That's what's happening.

The fix is trivial, if somewhat confusing.

dawehner’s picture

When we

+++ b/core/modules/rest/src/Plugin/ResourceBase.php
@@ -191,14 +196,30 @@ public function availableMethods() {
+    // Every route MUST have requirements that result in the access manager
+    // having access checks to check. If it does not, the route is made
+    // inaccessible. So, we default to granting access to everyone. If a
+    // permission exists, then we add that below. The access manager requires
+    // that ALL access checks must grant access, so this still results in
+    // correct behavior.
+    $requirements = array(
+      '_access' => 'TRUE',
+    );
+
+    // Only specify route requirements if the default permission exists. For any
+    // more advanced route definition, resource plugins extending this base
+    // class must override this method.
+    $permission = "restful $lower_method $this->pluginId";
+    if (isset($this->permissions()[$permission])) {
+      $requirements['_permission'] = $permission;
+    }

In an ideal world we would move this requirement building into its own subclass, so entityBase could simply override the behaviour. Here we just add more complexity to the already existing method.

Wim Leers’s picture

This removes the stopgap that #11 added to allow all RESTTestBase-based tests to still pass. In other words: it updates the tests to test the new reality. We have one test that verifies the old behavior still works.

So, this fixes #12.3 because that code is now gone, but we still need upgrade path tests.

Finally, this will have a fail in AuthTest. Analysis for that to follow.

Status: Needs review » Needs work

The last submitted patch, 24: rest_permissions-2664780-23.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
16.16 KB
752 bytes

Also fails in ResponseGeneratorTest, but that's not something tricky, it's just something I overlooked. Fixing that first, then addressing AuthTest.

This should now have a single fail, in AuthTest.

Status: Needs review » Needs work

The last submitted patch, 26: rest_permissions-2664780-26.patch, failed testing.

Wim Leers’s picture

#24 had a fail in AuthTest, because:

BC route requirements:
requirements:
  _access: 'TRUE'
  _permission: 'restful entity:node GET'
Non-BC route requirements:
requirements:
  _access: 'TRUE'

In the BC case, AuthTest will throw a 403 HTTP exception because the anonymous user does not have the permission, which \Drupal\Core\EventSubscriber\AuthenticationSubscriber::onExceptionSendChallenge() catches, which in turn causes \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException() to be called, which converts that into a 401 exception.

In the non-BC case, AuthTest will NOT throw a 403 HTTP exception because the anonymous user is not denied access, since the permission is no longer required. However, the anonymous user still cannot access the entity, because EntityResource::get() calls $entity_access = $entity->access('view', NULL, TRUE);, which calls \Drupal\entity_test\EntityTestAccessControlHandler::checkAccess(), which denies access, which results in EntityResource::get() throwing a 403 HTTP exception. Sounds like we're good? No, we're not! Because this time, the 403 exception does not bubble up to the HTTP kernel level, but is caught much earlier, in RequestHandler::handle(): since #1865594: Better REST error messages in case of exceptions, we catch any HTTP exceptions there and generate a response with an error message. Which means we still send a 403 response, but not a 403 exception. Since there is no 403 HTTP exception, the BasicAuth authentication provider (called by the HTTP kernel) also can't convert it into a 401 exception, which means … the anonymous user gets to access our response without being authenticated!!!

There are three two possible solutions:

  1. remove the "better error messages" stuff that #1865594: Better REST error messages in case of exceptions added, but then we lose our helpful error messages
  2. make the "better error messages" stuff that [#2865594] added more specific: either only convert 400 and 422, or convert everything except 403
  3. add _entity_access: '<entity type>:<operation>' route requirements, e.g. _entity_access: 'node.view' — this will result in the access checking happening before EntityResource::get() is called. This was simply not considered in #1866908: Honor entity and field access control in REST Services because _entity_access did not yet exist then: it was only added many months later, in #1947432: Add a generic EntityAccessCheck to replace entity_page_access().
    However… upon closer inspection, this causes failures for the REST routes that allow users to create entities: _entity_access only actually works for routes that have a route parameter that is that entity, which obviously can't work for POST routes. Patch attached for completeness, but expect failures.

So, there are not two possible solutions, but just one: #2 in the list above. (Even though I would have very much preferred the third option that uses _entity_access, it feels the most elegant to me personally.) We have variants A and B to choose from, and variant B seems to be the most reliable one.

P.S.: this is only somewhat related, but I want to point out that this seems like a very fragile design: we don't actually require authentication: _auth is a route option, not a route requirement. We only send a 401 if a 403 happens to occur, and that 403 is actually an unhandled exception: a 403 response won't be converted into a 401. This weakness was introduced in #1865594: Better REST error messages in case of exceptions.

Wim Leers’s picture

FileSize
1.46 KB

Forgot the interdiff for rest_permissions-2664780-28-3.patch.

Wim Leers’s picture

Wim Leers’s picture

So, running with option #28.2.b. Added documentation.

Interdiff relative to #26.


#23:

In an ideal world we would move this requirement building into its own subclass, so entityBase could simply override the behaviour. Here we just add more complexity to the already existing method.

I disagree. The whole point of doing this in ResourceBase rather than in EntityResource is to allow any resource plugin that extends ResourceBase to override permissions() to not specify any permissions, and then have it work.

The alternative is to specifically document ResourceBase to only be used for resources that need to have permissions generated, that don't have their own access control mechanism. But doing so would require EntityResource to no longer extend ResourceBase, which means it'd need to duplicate all of the complexity in ResourceBase::routes().

Given that ::permissions() is in fact defined by the ResourceInterface, and thus every REST resource plugin must implement it, it seems more appropriate to let ResourceBase::getBaseRoute() be smart enough to deal with a permissions() implementation that simply returns no permissions. And that's exactly what that code that you cited does.

The last submitted patch, 28: rest_permissions-2664780-28-2-a.patch, failed testing.

The last submitted patch, 28: rest_permissions-2664780-28-3.patch, failed testing.

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
Issue tags: -Needs upgrade path tests
FileSize
21.7 KB
4.91 KB

After fighting my way through many of the WTFs that one encounters when writing an upgrade test, here is what I believe to be a very solid update test.

IMHO this patch is ready.

Wim Leers’s picture

Issue summary: View changes

Updated IS to summarize the entire issue.

tedbow’s picture

+1 for this issue

I do find the rest permission confusing.

+++ b/core/modules/rest/src/Plugin/ResourceBase.php
@@ -191,14 +196,30 @@ public function availableMethods() {
+    // Every route MUST have requirements that result in the access manager
+    // having access checks to check. If it does not, the route is made
+    // inaccessible. So, we default to granting access to everyone. If a
+    // permission exists, then we add that below. The access manager requires
+    // that ALL access checks must grant access, so this still results in
+    // correct behavior.
+    $requirements = array(
+      '_access' => 'TRUE',
+    );
+
+    // Only specify route requirements if the default permission exists. For any
+    // more advanced route definition, resource plugins extending this base
+    // class must override this method.
+    $permission = "restful $lower_method $this->pluginId";
+    if (isset($this->permissions()[$permission])) {
+      $requirements['_permission'] = $permission;
+    }

Would it make sense after this block to have a have an else block that would unset($requirements['_access']);

I know technically it would behave no differently but if I was developer debugging and altering this route in a subscriber I know I would be confused if I saw '_access' == true and '_permission' == 'restful update node'

The comment block explains why effectively it still works but if someone is doing an alter they are likely not going to find this comment.

tedbow’s picture

+++ b/core/modules/rest/rest.install
@@ -21,3 +21,23 @@ function rest_requirements($phase) {
+function rest_update_8201() {
+  $config_factory = \Drupal::configFactory();
+  $rest_settings = $config_factory->getEditable('rest.settings');
+  $rest_settings->set('bc_entity_resource_permissions', TRUE)
+    ->save(TRUE);
+}

+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -234,6 +279,21 @@ protected function validate(EntityInterface $entity) {
+  public function permissions() {
+    // @see https://www.drupal.org/node/2664780
+    if ($this->configFactory->get('rest.settings')->get('bc_entity_resource_permissions')) {
+      // The default Drupal 8.0.x and 8.1.x behavior.
+      return parent::permissions();
+    }
+    else {
+      // The default Drupal 8.2.x behavior.
+      return [];
+    }
+  }

Could we check to see if any permissions defined by any rest resources that extend EntityResource has actually been assigned to a role?

If not would it make sense not to set this flag?

Just thinking of a site admin who thought they were going to use rest but never ended up using it or hadn't start before 8.2. In that case when did start using rest they would be stuck with the old permissions behavior unless they updated via command line.

Of course they would also even have to know they were using a deprecated permission system. To update via the command line.

This would probably be more likely to happen to sites that installed rest shortly before 8.2.x came out.

Wim Leers’s picture

Thanks for the review!

#36: I think we should keep it as it is, precisely to make it the least dynamic possible. Every developer should learn that all access checks must grant access to be allowed access a route. Hopefully, if a developer sees both, they'll learn that. Furthermore, if they see those two requirements, then they can also see the route name. Given the route name, they will surely find their way back here. And then they'll find the comment documenting this. That's exactly why I have that comment in the first place: to make sure all developers implementing or debugging @RestResource plugins based on ResourceBase are explicitly aware of this.

#37: Ohhh! That's very interesting :) But it also makes the BC-vs-new behavior far more difficult to understand: all new sites have it, none of the existing sites, EXCEPT …. Also, however much I like your very clever idea, I'm afraid we cannot actually rely on it: users with the administrator role are automatically granted every permission, yet the user.role.administrator configuration lists no permissions at all. So, it's perfectly possible that a site actually is relying on permission-based REST access, even if no EntityResource permissions are listed explicitly!
So, AFAICT your suggestion is:

  1. undesirable for clarity
  2. impossible due to roles having is_admin not listing which permissions they have
dawehner’s picture

I disagree. The whole point of doing this in ResourceBase rather than in EntityResource is to allow any resource plugin that extends ResourceBase to override permissions() to not specify any permissions, and then have it work.

Oh damnit I actually wanted to write submethod.

Wim Leers’s picture

Assigned: Unassigned » Wim Leers
Status: Needs review » Needs work

Oh, a helper method. that is totally reasonable.

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
Status: Needs work » Needs review
FileSize
22.53 KB
2.7 KB

Done. Extracted that logic from getBaseRoute() into getBaseRouteRequirements().

Status: Needs review » Needs work

The last submitted patch, 41: rest_permissions-2664780-41.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
22.57 KB

Hah, #2626298: REST module must cache only responses to GET requests just landed, causing this to fail. Rebased. One tiny conflict to resolve.

Status: Needs review » Needs work

The last submitted patch, 43: rest_permissions-2664780-43.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
24.13 KB
1.59 KB

#2626298: REST module must cache only responses to GET requests also expanded some test coverage in a non-conflicting way, which explains the fail in #43. Fixed.

dawehner’s picture

\Drupal\entity_test\EntityTestAccessControlHandler::checkAccess(), which denies access, which results in EntityResource::get() throwing a 403 HTTP exception. Sounds like we're good? No, we're not! Because this time, the 403 exception does not bubble up to the HTTP kernel level, but is caught much earlier, in RequestHandler::handle()

Here is a question. Why does \Drupal\rest\Plugin\rest\resource\EntityResource::getBaseRoute not specify _entity_access: $type.view. Wouldn't that allow us to not deal with it?

+++ b/core/modules/rest/src/RequestHandler.php
@@ -91,6 +91,16 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
     catch (HttpException $e) {
+      // In case a response is forbidden for the current user, we must not
+      // convert it into a response, because that would rob
+      // \Drupal\Core\EventSubscriber\AuthenticationSubscriber::onExceptionSendChallenge()
+      // from the opportunity to convert it into a 401 response, to challenge
+      // the user to authenticate.
+      // @see \Drupal\Core\Authentication\AuthenticationProviderChallengeInterface
+      if ($e->getStatusCode() == 403) {
+        throw $e;
+      }

Would it make more sense to catch AccessDeniedHttpExceptions separately?

Wim Leers’s picture

#46:

[…] _entity_access […]

No, that doesn't work. I tried! See #28, option 3 for the details + patch.

Would it make more sense to catch AccessDeniedHttpExceptions separately?

I'd love to! But REST has lots of throw new HttpException(STATUSCODE, …), which means it's more likely than not that contrib plugins will do the same. Which means we can't 100% rely on catching that particular exception. If you think we can, that'd be fine for me.

dawehner’s picture

No, that doesn't work. I tried! See #28, option 3 for the details + patch.

However… upon closer inspection, this causes failures for the REST routes that allow users to create entities: _entity_access only actually works for routes that have a route parameter that is that entity, which obviously can't work for POST routes. Patch attached for completeness, but expect failures.

Right, this is why we _entity_create_access. On the other hand I see what you mean. Arbitrary code could still execute so we want to have a special behaviour for it.

I'd love to! But REST has lots of throw new HttpException(STATUSCODE, …), which means it's more likely than not that contrib plugins will do the same. Which means we can't 100% rely on catching that particular exception. If you think we can, that'd be fine for me.

Ah too bad, yeah nevermind.

Wim Leers’s picture

Arbitrary code could still execute so we want to have a special behaviour for it.

Exactly!

(Thanks for the pointer though, I should have found that myself.)


So … what's left here?

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Works for me now

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 45: rest_permissions-2664780-45.patch, failed testing.

dawehner’s picture

ha, this is this time an actual failing test ...

Wim Leers’s picture

Assigned: Unassigned » Wim Leers

Yep, one of the recent commits introduced another usage of this permission. I'll fix this.

Wim Leers’s picture

Status: Needs work » Needs review
Related issues: +#2737751: Refactor REST routing to address its brittleness
FileSize
24.59 KB
788 bytes

Confirmed, #2631774: Impossible to update Comment entity with REST (HTTP PATCH): bundle field not allowed to be updated, but EntityNormalizer::denormalize() requires it introduced another use of the permission that this patch now disables by default. Simply removing that permission should do the trick.


So, it does the trick in that it solves the fail we saw above, but … then we start to see new fails (i.e. this patch will fail), triggered by this hunk in the patch:

+++ b/core/modules/rest/src/RequestHandler.php
@@ -91,6 +91,16 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
+      // In case a response is forbidden for the current user, we must not
+      // convert it into a response, because that would rob
+      // \Drupal\Core\EventSubscriber\AuthenticationSubscriber::onExceptionSendChallenge()
+      // from the opportunity to convert it into a 401 response, to challenge
+      // the user to authenticate.
+      // @see \Drupal\Core\Authentication\AuthenticationProviderChallengeInterface
+      if ($e->getStatusCode() == 403) {
+        throw $e;
+      }
+
       $error['error'] = $e->getMessage();
       $content = $serializer->serialize($error, $format);
       // Add the default content type, but only if the headers from the

Those additions mean that a 403 exception no longer gets such an {error: "ERROR MESSAGE"} response. Which was fine so far, but #2631774: Impossible to update Comment entity with REST (HTTP PATCH): bundle field not allowed to be updated, but EntityNormalizer::denormalize() requires it added test coverage for that. And so now it turns out this solution isn't good enough anymore.

This is just yet another example for #2737751: Refactor REST routing to address its brittleness. :(

Wim Leers’s picture

Clearly, the real problem is that this catching of all exceptions in the controller and transforming it into a response is something that:

  1. works against Symfony's design, where you should really use a subscriber to the KernelEvents::EXCEPTION event to address this
  2. works against Drupal's design, which builds on top of the aforementioned Symfony design, and provides a base class: \Drupal\Core\EventSubscriber\HttpExceptionSubscriberBase

So, let's get rid of all that logic in RequestHandler and instead use that established pattern. That means we need a subclass of HttpExceptionSubscriberBase that can handle all serialization formats. The RTBC patch at #2403307: RPC endpoints for user authentication: log in, check login status, log out already has that, so we can import that from there (and when that lands, we can remove it from this patch).

That's what this patch does.

dawehner’s picture

Nice!

The last submitted patch, 54: rest_permissions-2664780-54.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 55: rest_permissions-2664780-55.patch, failed testing.

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
Status: Needs work » Needs review
FileSize
37.72 KB
9.53 KB

So, we made progress in #54, but we're still far from where we need to be… there's still lots of fails :/


The first problem is that we're now getting error (HTTP 4xx) responses in HTML, not in JSON/HAL+JSON/… Why? Because none of the PATCH or POST tests actually specify ?_format=SOME_FORMAT in the URL. Which means we don't really request any format, and hence \Drupal\Core\StackMiddleware\NegotiationMiddleware::getContentType() defaults to html.

Possible solutions:

  1. Add ?_format=SOME_FORMAT to all POST and PATCH requests.
  2. Do #2662284-33: Return complete entity after successful PATCH, -35, -36, but that would be hugely out of scope.

So, I went with solution 1.


After the above is solved, the second problem is that in HEAD, the error message lives in an error key, but as of #55, it lives in a message key. Is this a BC break? I don't think so: it's only intended for developers anyway. So I went with that for now.
EDIT: Alternatively, we could change the new exception subscriber to put the message at 'error' instead of at 'message'.

And a third problem is that HttpExceptionSubscriberBase itself is poorly designed: it requires every HTTP status to have its own callback, and it's missing the on422() callback by default. So, added that on422() method.

dawehner’s picture

And a third problem is that HttpExceptionSubscriberBase itself is poorly designed: it requires every HTTP status to have its own callback, and it's missing the on422() callback by default. So, added that on422() method.

Well, it was designed to solve a specific problem, but there is no reason to not solve that: #2739617: Make it easier to write on4xx() exception subscribers

After the above is solved, the second problem is that in HEAD, the error message lives in an error key, but as of #55, it lives in a message key. Is this a BC break? I don't think so: it's only intended for developers anyway. So I went with that for now.
EDIT: Alternatively, we could change the new exception subscriber to put the message at 'error' instead of at 'message'.

Given that something is wrong on an exception anyway I don't think this BC break is a real BC break.

Possible solutions:

Add ?_format=SOME_FORMAT to all POST and PATCH requests.
Do #2662284-33: Return complete entity after successful PATCH, -35, -36, but that would be hugely out of scope.
So, I went with solution 1.

Does that mean we commit this issue after the issue on #2662284: Return complete entity after successful PATCH and call it a day?

Wim Leers’s picture

Title: Remove REST's resource-and-verb-specific permissions for EntityResource, but provide BC, document why it's necessary for other resources » [PP-2] Remove REST's resource-and-verb-specific permissions for EntityResource, but provide BC, document why it's necessary for other resources
Status: Needs review » Postponed

Thanks for opening #2739617: Make it easier to write on4xx() exception subscribers!

Does that mean we commit this issue after the issue on #2662284: Return complete entity after successful PATCH and call it a day?

Yes! Committing this patch after that one will make this patch a fair bit simpler. I just went with option 1 for now, to prove that this patch can in fact be green.

And in fact, if #2403307: RPC endpoints for user authentication: log in, check login status, log out lands first, we'll also be able to remove most of what #55 changed.

So, let's postpone this on both #2662284 and #2403307.

Wim Leers’s picture

Title: [PP-2] Remove REST's resource-and-verb-specific permissions for EntityResource, but provide BC, document why it's necessary for other resources » [PP-1] Remove REST's resource-and-verb-specific permissions for EntityResource, but provide BC, document why it's necessary for other resources
dawehner’s picture

It feels weird to block on a conceptual different issue, just because it shares some code. Maybe I don't get it, but this one shared class feels not like a big deal when a potential reroll needs to happen.

Wim Leers’s picture

I agree. But that's due to the unfortunate state that the rest.module codebase is in. The fix in #55 (see the interdiff) is about 20% of the entire patch size. There's been many more eyeballs on #2403307: RPC endpoints for user authentication: log in, check login status, log out, so I'd rather see that hunk landing there than here.

Furthermore, #2403307: RPC endpoints for user authentication: log in, check login status, log out has been ready for almost 2 months at this point, so I expected it to be committed a long time ago. Hopefully any day now.

Wim Leers’s picture

Title: [PP-1] Remove REST's resource-and-verb-specific permissions for EntityResource, but provide BC, document why it's necessary for other resources » Remove REST's resource-and-verb-specific permissions for EntityResource, but provide BC, document why it's necessary for other resources
Assigned: Unassigned » Wim Leers
Status: Postponed » Needs work

The changes made in #55 actually aren't being introduced in #2403307: RPC endpoints for user authentication: log in, check login status, log out anymore, they already were introduced in #2308745: Remove rest.settings.yml, use rest_resource config entities! So, no need for this to be postponed anymore.

Rerolling…

Wim Leers’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
32.79 KB
1.84 KB

rest_update_8201() now exists already, so renamed the update function in this patch to rest_update_8203() (this is what the interdiff shows). Updated the patch + IS accordingly. The CR is still accurate.

Wim Leers’s picture

After #59 was rolled, #2308745: Remove rest.settings.yml, use rest_resource config entities landed, which added a administer rest resources permission. Hence the assumption that was made in this issue/patch that \Drupal\rest\Tests\Update\EntityResourcePermissionsUpdateTest makes about REST module having no permissions by default is no longer true.

This reroll/interdiff makes the minor changes necessary to no longer have that assumption, so this patch should come back green.

(I also noticed that the test method name was silly/wrong, it was a copy/paste remnant. Fixed that too.)

The last submitted patch, 66: rest_permissions-2664780-66.patch, failed testing.

tedbow’s picture

Looking at #67 interdiff. It handles the new default permission for REST, ''administer rest resources'" correctly in the test. That was the only test fail in #66

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

ok just went through comments in #51 and it looks like we are back to RTBC.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 67: rest_permissions-2664780-67.patch, failed testing.

tedbow’s picture

Status: Needs work » Needs review
FileSize
27.54 KB

Here is just automatic re-roll no changes. If tests pass I think RTBC

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

RTBC b/c patch not changed since #70

alexpott’s picture

Issue tags: +beta deadline, +beta target

Discussed with @catch, @effulgentsia, and @cottser we agreed that this needs to be done by the time 8.2.x beta is cut - but it is also an important patch because the last piece of a jigsaw that we want to complete in 8.2.0.

Wim Leers’s picture

but it is also an important patch because the last piece of a jigsaw that we want to complete in 8.2.0.

Nice way of putting it! :)

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 72: 2752325-72.patch, failed testing.

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
32.15 KB

Conflicted with #2403307: RPC endpoints for user authentication: log in, check login status, log out, trivial resolution.

(Rebased from #67, because #72 lost the upgrade path test.)

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests
16:18 alexpott
WimLeers: I'm not sure testCreateWithoutPermissionIfBcFlagIsOn is enough
16:19 alexpott
WimLeers: all we're testing there is that when this is TRUE you are denied access
16:19 WimLeers
alexpott: Yes, because the permission is not granted to the user that tries to POST
16:19 WimLeers
alexpott: So what would you like to see beyond this?
16:19 WimLeers
alexpott: surely you don't want _every_ test to be testing both with and without the permission?
16:19 alexpott
WimLeers: there's no test coverage that the old permissions still exist when this layer is on
16:20 alexpott
WimLeers: and there's no coverage of whether or not when this layer is on the permissions work
16:21 _ACF_ has joined (~ACF@host86-179-62-188.range86-179.btcentralplus.com)
16:21 alexpott
WimLeers: I don't think every test needs both but I do think we need a little more coverage to prove that we've not broken something that was relied upon
16:21 WimLeers
alexpott: +1

One of my major concern's is that everything probably works great right now but with minimum of test coverage future us's might break this.

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community
Issue tags: -Needs tests
FileSize
32.88 KB
1.05 KB

there's no coverage of whether or not when this layer is on the permissions work

This fixes that.

there's no test coverage that the old permissions still exist when this layer is on

Actually, we do have test coverage for that, in \Drupal\rest\Tests\Update\EntityResourcePermissionsUpdateTest::testBcEntityResourcePermissionSettingAdded():

    $this->runUpdates();

    // Make sure we have the expected values after the update.
    $rest_settings = $this->config('rest.settings');
    $this->assertTrue(array_key_exists('bc_entity_resource_permissions', $rest_settings->getRawData()));
    $this->assertTrue($rest_settings->get('bc_entity_resource_permissions'));
    $rest_permissions = array_keys(array_filter($permission_handler->getPermissions(), $is_rest_resource_permission));
    $this->assertEqual(['restful delete entity:node', 'restful get entity:node', 'restful patch entity:node', 'restful post entity:node'], $rest_permissions);

So, AFAICT we were really just missing that one bit?

xjm’s picture

Title: Remove REST's resource-and-verb-specific permissions for EntityResource, but provide BC, document why it's necessary for other resources » Remove REST's resource- and verb-specific permissions for EntityResource, but provide BC and document why it's necessary for other resources

(Retitling for grammar nitpicking by @prestonso and myself. Carry on.)

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Less permissions - simpler. Nice. Committed 85d755a and pushed to 8.2.x. Thanks!

diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php
index 6ec0ea4..22b8b51 100644
--- a/core/modules/page_cache/src/Tests/PageCacheTest.php
+++ b/core/modules/page_cache/src/Tests/PageCacheTest.php
@@ -8,7 +8,6 @@
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\simpletest\WebTestBase;
 use Drupal\Core\Cache\Cache;
-use Drupal\user\Entity\Role;
 use Drupal\user\RoleInterface;
 
 /**
diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php
index bbc0f9c..088dca2 100644
--- a/core/modules/rest/src/RequestHandler.php
+++ b/core/modules/rest/src/RequestHandler.php
@@ -12,7 +12,6 @@
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpKernel\Exception\HttpException;
 use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 use Symfony\Component\Serializer\SerializerInterface;

Unused uses fixed on commit.

  • alexpott committed 85d755a on 8.2.x
    Issue #2664780 by Wim Leers, tedbow, dawehner, klausi: Remove REST's...
Wim Leers’s picture

Woot!

  • alexpott committed 23ed767 on 8.2.x
    Revert "Issue #2664780 by Wim Leers, tedbow, dawehner, klausi: Remove...
  • alexpott committed 2ec2334 on 8.2.x
    Issue #2664780 by Wim Leers, tedbow, dawehner, klausi: Remove REST's...
dawehner’s picture

Nice! Nice work @Wim Leers and @tedbow

Wim Leers’s picture

Thanks :)

tedbow’s picture

Thanks!

xjm’s picture

Issue tags: -beta target

In before the bell, so untagging. Yay!

xjm’s picture

Issue tags: +8.2.0 release notes

Also.

  • alexpott committed 23ed767 on 8.3.x
    Revert "Issue #2664780 by Wim Leers, tedbow, dawehner, klausi: Remove...
  • alexpott committed 2ec2334 on 8.3.x
    Issue #2664780 by Wim Leers, tedbow, dawehner, klausi: Remove REST's...
  • alexpott committed 85d755a on 8.3.x
    Issue #2664780 by Wim Leers, tedbow, dawehner, klausi: Remove REST's...

  • alexpott committed 23ed767 on 8.3.x
    Revert "Issue #2664780 by Wim Leers, tedbow, dawehner, klausi: Remove...
  • alexpott committed 2ec2334 on 8.3.x
    Issue #2664780 by Wim Leers, tedbow, dawehner, klausi: Remove REST's...
  • alexpott committed 85d755a on 8.3.x
    Issue #2664780 by Wim Leers, tedbow, dawehner, klausi: Remove REST's...

Status: Fixed » Closed (fixed)

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