This is a follow-up that was never filed for: #2939722: Do not provide routes for internal resource types

The motivation is that I would like to keep simplifying the RequestHandler/EntityResource. When the relationship field is a route parameter, we have to deal with more moving parts. E.g. if we get a request for `/jsonapi/node/foo/relationships/missingfield` we have to manually throw a 404 exception, rather than just letting the router handle that.

I already started working on this and just need to re-roll an older patch.

Comments

gabesullice created an issue. See original summary.

e0ipso’s picture

I think this is a better approach. I see us moving towards that path at some point. However I'd like to slate this for after core's inclusion. At this point I feel we ought to fix the remaining issues and move the bureaucracy forward.

What are your thoughts?

gabesullice’s picture

However, I'd like to slate this for after core's inclusion.

I don't think one should prevent the other. I think they can happen in parallel.

At this point I feel we ought to fix the remaining issues and move the bureaucracy forward.

I agree that it is time for us to push the bureaucracy forward and indeed, I think we are! #2836165-37: New experimental module: JSON API

wim leers’s picture

However I'd like to slate this for after core's inclusion.

If anything, this should happen before that. Architectural changes will be 10x slower once this is in core.

wim leers’s picture

I think this probably makes sense to move this to 2.x?

wim leers’s picture

Version: 8.x-1.x-dev » 8.x-2.x-dev

Tentatively moving.

gabesullice’s picture

Title: Statically define related/relationship routes » Define related/relationship routes per field, not dynamically

I wonder if my name for this issue makes the task sound like a much bigger undertaking than it is. Perhaps that "static" part could have been taken to mean "in YAML" or config or something like that.

By "statically," I just meant that the field name shouldn't be a route parameter and that we should create a route for each possible field, rather than handle the field dynamically.

Having a param just adds edge cases for us to handle unecessarily, since we really don't need to check anything "at runtime"

gabesullice’s picture

Version: 8.x-2.x-dev » 8.x-1.x-dev

This would be a completely non-breaking change AFAICT

wim leers’s picture

Title: Define related/relationship routes per field, not dynamically » Define related/relationship routes per field, not dynamically (with route parameters that needs validating)
Related issues: +#2960082: Spec Compliance: requests to "<resource>/relationships" should result in 404 response, currently results in fatal PHP error

I think you're right, that simpler framing of things does make the scope much clearer. This would also solve #2960082: Spec Compliance: requests to "<resource>/relationships" should result in 404 response, currently results in fatal PHP error.

The only complication I can think of is that this would require route rebuilds whenever fields are added. We'll need something like \Drupal\rest\EventSubscriber\RestConfigSubscriber to trigger a route rebuild whenever entity or field config are modified.

wim leers’s picture

Wim Leers credited BR0kEN.

wim leers’s picture

Crediting @BR0kEN.

wim leers’s picture

Because of #9/#10, this also affects DX. Tagging.

wim leers’s picture

Title: Define related/relationship routes per field, not dynamically (with route parameters that needs validating) » Define related/relationship routes per field, not dynamically (with route parameters that need validating)

Title typo.

gabesullice’s picture

Version: 8.x-1.x-dev » 8.x-2.x-dev
gabesullice’s picture

StatusFileSize
new7.99 KB
wim leers’s picture

  1. +++ b/src/Access/RelationshipFieldAccess.php
    @@ -0,0 +1,56 @@
    +    $is_mutation_request = in_array($request->getMethod(), ['POST', 'PATCH', 'DELETE']);
    

    Could be changed to !$request->isMethodSafe(). Don't feel strongly about it though, I'll let you choose.

  2. +++ b/src/Routing/Routes.php
    @@ -230,25 +231,29 @@ class Routes implements ContainerInjectionInterface {
    -    // @todo: remove this when each related route is defined per relationship field and access is no longer checked by the controller in https://www.drupal.org/project/jsonapi/issues/2953346.
    -    $related_route->setRequirement('_access', 'TRUE');
    ...
    +      $related_route->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name);
    

    This @todo is addressed 👍

  3. +++ b/src/Routing/Routes.php
    @@ -230,25 +231,29 @@ class Routes implements ContainerInjectionInterface {
    -    // @todo: remove the _on_relationship default in https://www.drupal.org/project/jsonapi/issues/2953346.
    -    $relationship_route->addDefaults(['_on_relationship' => TRUE]);
    ...
    +      // @todo: remove the _on_relationship default in https://www.drupal.org/project/jsonapi/issues/2953346.
    +      $relationship_route->addDefaults(['_on_relationship' => TRUE]);
    ...
    +      $relationship_route->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name);
    

    This one is not❓

  4. +++ b/src/Routing/Routes.php
    @@ -230,25 +231,29 @@ class Routes implements ContainerInjectionInterface {
    +      $related_route->addDefaults(['related' => $relationship_field_name]);
    ...
    +      $relationship_route->addDefaults(['related' => $relationship_field_name]);
    

    Can't we now also remove these?

gabesullice’s picture

StatusFileSize
new12.32 KB
new18.49 KB

Haven't seriously addressed the review in #17, this is still a WIP. Although I did take the isMethodSafe suggestion.

This primarily gets rid of the access checks that the route-level access check makes obsolete.

One notable change that this introduces is that the errors no longer contain a pointer. That makes sense, there's no document to point at. Pointers only make sense when PATCHing a resource. They "point" to errors in the submission.

gabesullice’s picture

The last submitted patch, 16: 2953346-16.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Needs review » Needs work

The last submitted patch, 18: 2953346-18.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

gabesullice’s picture

StatusFileSize
new9.04 KB
new24.99 KB

Handles the low-hanging fruit in those failures.

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new5.63 KB
new29.96 KB

More orchard harvesting.


+++ b/tests/src/Kernel/Controller/EntityResourceTest.php
@@ -767,86 +767,6 @@ class EntityResourceTest extends JsonapiKernelTestBase {
-  public function testGetRelatedInternal() {
...
-  public function testGetRelationshipInternal() {
...
-  public function testCreateRelationshipInternal() {
...
-  public function testPatchRelationshipInternal() {
...
-  public function testDeleteRelationshipInternal() {

These kernel tests can all be removed because EntityResource no longer needs to check for 404s. Those routes don't even exist anymore!

Status: Needs review » Needs work

The last submitted patch, 23: 2953346-23.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

gabesullice’s picture

StatusFileSize
new2.79 KB
new31.54 KB

Code standards.

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new5.22 KB
new33.04 KB

🤞should be green.

Even if green, still not done. The next step will be to convert/remove the related route default (mentioned in #17.4). I'm not sure that we should remove it entirely, it's still useful as a parameter, but maybe it should be a route parameter (under options) rather than a default.

gabesullice’s picture

So happy to finally be doing this :)

  1. +++ b/tests/src/Functional/JsonApiFunctionalTest.php
    @@ -772,7 +772,7 @@ class JsonApiFunctionalTest extends JsonApiFunctionalTestBase {
    -    $this->assertEquals(403, $response->getStatusCode());
    +    $this->assertEquals(401, $response->getStatusCode());
    

    Now, we're actually getting a correct error code :)

  2. +++ b/src/Controller/EntityResource.php
    @@ -393,12 +391,9 @@ class EntityResource {
    -    $related_field = $this->resourceType->getInternalName($related_field);
    -    $this->relationshipAccess($entity, 'view', $related_field);
    ...
    -    $this->validateReferencedResource($field_list, $related_field);
    
    @@ -458,41 +453,13 @@ class EntityResource {
    -    $related_field = $this->resourceType->getInternalName($related_field);
    -    $this->relationshipAccess($entity, 'view', $related_field);
    ...
    -    $this->validateReferencedResource($field_list, $related_field);
    ...
    -  protected function validateReferencedResource($field_list, $related_field) {
    
    @@ -518,7 +485,6 @@ class EntityResource {
    -    $this->relationshipAccess($entity, 'update', $related_field);
    
    @@ -619,7 +585,6 @@ class EntityResource {
    -    $this->relationshipAccess($entity, 'update', $related_field);
    
    @@ -708,8 +673,6 @@ class EntityResource {
    -    $this->relationshipAccess($entity, 'update', $related_field);
    
    @@ -879,9 +842,6 @@ class EntityResource {
    -    if (!($field_list = $entity->get($related_field)) || !$this->isRelationshipField($field_list)) {
    -      throw new NotFoundHttpException(sprintf('The relationship %s is not present in this resource.', $related_field));
    -    }
    
    @@ -965,25 +925,6 @@ class EntityResource {
    -  protected function isRelationshipField(FieldItemListInterface $entity_field) {
    

    All of the above "request-time" logic is replaced by:

    +++ b/src/Access/RelationshipFieldAccess.php
    @@ -0,0 +1,65 @@
    +/**
    + * Defines a class to check access to related and relationship routes.
    ...
    + */
    +class RelationshipFieldAccess implements AccessInterface {
    
    ...
    
    +++ b/src/Routing/Routes.php
    @@ -230,25 +231,33 @@ class Routes implements ContainerInjectionInterface {
    +    foreach ($resource_type->getRelatableResourceTypes() as $relationship_field_name => $target_resource_types) {
    ...
    +      $relationship_route->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name);
    ...
    +      $routes->add(static::getRouteName($resource_type, "$relationship_field_name.relationship"), $relationship_route);
    ...
    +      if (static::hasNonInternalTargetResourceTypes($target_resource_types)) {
    ...
    +        $related_route->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name);
    +        $routes->add(static::getRouteName($resource_type, "$relationship_field_name.related"), $related_route);
    +      }
    
  3. 🙂

gabesullice’s picture

Assigned: gabesullice » Unassigned
StatusFileSize
new651 bytes
new32.94 KB

#17.1: Done, changed to isMethodCacheable() because of a deprecation warning.
2. ✔️
3. I just tried to do this, but I don't think it makes sense to do. I think that todo shouldn't have been put there. I think it had more to do with the normalizer's use of the request object than that default being a bad one.
4. #16 discusses this. It's a correct and useful value, it doesn't need to be removed. Removing it would mean having to inspect the route object again, which we tried hard to remove.

wim leers’s picture

Status: Needs review » Needs work
  • #18++
  • #22++
  • #23++ (also: More orchard harvesting. 😂)
  • #28.3 & #28.4: ok. Those were just hopes about this bringing even more clean-up/consistency!
  1. +++ b/src/Access/RelationshipFieldAccess.php
    @@ -0,0 +1,65 @@
    + * @package Drupal\jsonapi\Access
    

    Nit: Let's remove this.

  2. +++ b/src/Access/RelationshipFieldAccess.php
    @@ -0,0 +1,65 @@
    +        if ($access_result instanceof AccessResultReasonInterface) {
    +          $reason = "The current user is not allowed to {$field_operation} this relationship.";
    +          if ($access_reason = $access_result->getReason()) {
    +            $reason .= " {$access_reason}";
    +          };
    +          $access_result->setReason($reason);
    +        }
    

    Even if there is no reason, I think we still want that default message to be shown. We can transform a reasonless access result into one that has at least the generic text.

  3. +++ b/src/Controller/EntityResource.php
    @@ -518,7 +485,6 @@ class EntityResource {
       public function createRelationship(EntityInterface $entity, $related_field, $parsed_field_list, Request $request) {
    

    ::createRelationship() and deleteRelationship() are still explicitly calling $field_list->access(). I think that already was unnecessary.

    (Actually, relationshipAccess() was being called with

    $operation =
    'update'

    , and that method called entity access and field access with that operation. That's fine for entity access, but WRONG for field access. Thanks to the explicit extra field access check — the one I'm asking you to remove — a security vulnerability was averted! 😱)

  4. +++ b/src/Controller/EntityResource.php
    @@ -518,7 +485,6 @@ class EntityResource {
    -    $this->relationshipAccess($entity, 'update', $related_field);
    

    🎉

    You still need to remove \Drupal\jsonapi\Controller\EntityResource::relationshipAccess() :D

  5. +++ b/tests/src/Functional/JsonApiFunctionalTest.php
    @@ -723,9 +723,8 @@ class JsonApiFunctionalTest extends JsonApiFunctionalTestBase {
    -      'related' => 'field_tags',
    

    🎉

gabesullice’s picture

Status: Needs work » Needs review
StatusFileSize
new5.24 KB
new35.87 KB

Addresses #29.

wim leers’s picture

Status: Needs review » Reviewed & tested by the community

👌

The last submitted patch, 25: 2953346-25.patch, failed testing. View results

wim leers’s picture

Status: Reviewed & tested by the community » Fixed

Status: Fixed » Closed (fixed)

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