diff --git a/jsonapi.services.yml b/jsonapi.services.yml index b5c3f22..f059898 100644 --- a/jsonapi.services.yml +++ b/jsonapi.services.yml @@ -113,6 +113,10 @@ services: jsonapi.field_resolver: class: Drupal\jsonapi\Context\FieldResolver arguments: ['@entity_type.manager', '@entity_field.manager', '@entity_type.bundle.info', '@jsonapi.resource_type.repository'] + access_check.jsonapi.relationship_field_access: + class: Drupal\jsonapi\Access\RelationshipFieldAccess + tags: + - { name: access_check, applies_to: _jsonapi_relationship_field_access, needs_incoming_request: TRUE } paramconverter.jsonapi.entity_uuid: class: Drupal\jsonapi\ParamConverter\EntityUuidConverter tags: diff --git a/src/Access/RelationshipFieldAccess.php b/src/Access/RelationshipFieldAccess.php new file mode 100644 index 0000000..3e9622a --- /dev/null +++ b/src/Access/RelationshipFieldAccess.php @@ -0,0 +1,56 @@ +getRequirement(static::ROUTE_REQUIREMENT_KEY); + $is_mutation_request = in_array($request->getMethod(), ['POST', 'PATCH', 'DELETE']); + $field_operation = $is_mutation_request ? 'edit' : 'view'; + $entity_operation = $is_mutation_request ? 'update' : 'view'; + if ($resource_type = Routes::getResourceTypeNameFromParameters($route->getDefaults())) { + $entity = $request->get($resource_type->getEntityTypeId()); + if ($entity instanceof FieldableEntityInterface && $entity->hasField($relationship_field_name)) { + $entity_access = $entity->access($entity_operation, $account, TRUE); + $field_access = $entity->get($relationship_field_name)->access($field_operation, $account, TRUE); + return $entity_access->andIf($field_access); + } + } + return AccessResult::neutral(); + } +} \ No newline at end of file diff --git a/src/Normalizer/Value/RelationshipNormalizerValue.php b/src/Normalizer/Value/RelationshipNormalizerValue.php index 57798ec..f193afc 100644 --- a/src/Normalizer/Value/RelationshipNormalizerValue.php +++ b/src/Normalizer/Value/RelationshipNormalizerValue.php @@ -149,14 +149,15 @@ class RelationshipNormalizerValue extends FieldNormalizerValue { * An array of links to be rasterized. */ protected function getLinks($field_name) { + $relationship_field_name = $this->resourceType->getPublicName($field_name); $route_parameters = [ - 'related' => $this->resourceType->getPublicName($field_name), + 'related' => $relationship_field_name, ]; $links['self'] = $this->linkManager->getEntityLink( $this->hostEntityId, $this->resourceType, $route_parameters, - 'relationship' + "$relationship_field_name.relationship" ); $resource_types = $this->resourceType->getRelatableResourceTypesByField($field_name); if (static::hasNonInternalResourceType($resource_types)) { @@ -164,7 +165,7 @@ class RelationshipNormalizerValue extends FieldNormalizerValue { $this->hostEntityId, $this->resourceType, $route_parameters, - 'related' + "$relationship_field_name.related" ); } return $links; diff --git a/src/Routing/Routes.php b/src/Routing/Routes.php index dbd5c47..2412f4d 100644 --- a/src/Routing/Routes.php +++ b/src/Routing/Routes.php @@ -3,6 +3,7 @@ namespace Drupal\jsonapi\Routing; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\jsonapi\Access\RelationshipFieldAccess; use Drupal\jsonapi\Controller\EntryPoint; use Drupal\jsonapi\Normalizer\Relationship; use Drupal\jsonapi\ParamConverter\ResourceTypeConverter; @@ -230,25 +231,29 @@ class Routes implements ContainerInjectionInterface { $routes->add(static::getRouteName($resource_type, 'individual.delete'), $individual_remove_route); } - // Get an individual resource's related resources. - $related_route = new Route("/{$path}/{{$entity_type_id}}/{related}"); - $related_route->setMethods(['GET']); - // @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'); - $routes->add(static::getRouteName($resource_type, 'related'), $related_route); - - // Read, update, add, or remove an individual resources relationships to - // other resources. - $relationship_route = new Route("/{$path}/{{$entity_type_id}}/relationships/{related}"); - $relationship_route->setMethods($resource_type->isMutable() - ? ['GET', 'POST', 'PATCH', 'DELETE'] - : ['GET'] - ); - // @todo: remove the _on_relationship default in https://www.drupal.org/project/jsonapi/issues/2953346. - $relationship_route->addDefaults(['_on_relationship' => TRUE]); - $relationship_route->addDefaults(['serialization_class' => Relationship::class]); - $relationship_route->setRequirement('_csrf_request_header_token', 'TRUE'); - $routes->add(static::getRouteName($resource_type, 'relationship'), $relationship_route); + foreach ($resource_type->getRelatableResourceTypes() as $relationship_field_name => $target_resource_type) { + // Get an individual resource's related resources. + $related_route = new Route("/{$path}/{{$entity_type_id}}/{$relationship_field_name}"); + $related_route->setMethods(['GET']); + $related_route->addDefaults(['related' => $relationship_field_name]); + $related_route->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name); + $routes->add(static::getRouteName($resource_type, "$relationship_field_name.related"), $related_route); + + // Read, update, add, or remove an individual resources relationships to + // other resources. + $relationship_route = new Route("/{$path}/{{$entity_type_id}}/relationships/{$relationship_field_name}"); + $relationship_route->setMethods($resource_type->isMutable() + ? ['GET', 'POST', 'PATCH', 'DELETE'] + : ['GET'] + ); + // @todo: remove the _on_relationship default in https://www.drupal.org/project/jsonapi/issues/2953346. + $relationship_route->addDefaults(['_on_relationship' => TRUE]); + $relationship_route->addDefaults(['serialization_class' => Relationship::class]); + $relationship_route->addDefaults(['related' => $relationship_field_name]); + $relationship_route->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name); + $relationship_route->setRequirement('_csrf_request_header_token', 'TRUE'); + $routes->add(static::getRouteName($resource_type, "$relationship_field_name.relationship"), $relationship_route); + } // Add entity parameter conversion to every route. $routes->addOptions(['parameters' => [$entity_type_id => ['type' => 'entity:' . $entity_type_id]]]);