diff --git a/src/Access/RelationshipFieldAccess.php b/src/Access/RelationshipFieldAccess.php index 3e9622a..eefb7f0 100644 --- a/src/Access/RelationshipFieldAccess.php +++ b/src/Access/RelationshipFieldAccess.php @@ -3,6 +3,7 @@ namespace Drupal\jsonapi\Access; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Access\AccessResultReasonInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Routing\Access\AccessInterface; use Drupal\Core\Session\AccountInterface; @@ -40,15 +41,18 @@ class RelationshipFieldAccess implements AccessInterface { */ public function access(Request $request, Route $route, AccountInterface $account) { $relationship_field_name = $route->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())) { + $field_operation = $request->isMethodSafe() ? 'view' : 'edit'; + $entity_operation = $request->isMethodSafe() ? 'view' : 'update'; + if ($resource_type = $request->get(Routes::RESOURCE_TYPE_KEY)) { $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); + $access_result = $entity_access->andIf($field_access); + if ($access_result instanceof AccessResultReasonInterface) { + $access_result->setReason("The current user is not allowed to $field_operation this relationship. " . $access_result->getReason()); + } + return $access_result; } } return AccessResult::neutral(); diff --git a/src/Controller/EntityResource.php b/src/Controller/EntityResource.php index b76b201..2d6235b 100644 --- a/src/Controller/EntityResource.php +++ b/src/Controller/EntityResource.php @@ -383,7 +383,7 @@ class EntityResource { /** * Gets the related resource. * - * @param \Drupal\Core\Entity\EntityInterface $entity + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity * The requested entity. * @param string $related_field * The related field name. @@ -393,12 +393,9 @@ class EntityResource { * @return \Drupal\jsonapi\ResourceResponse * The response. */ - public function getRelated(EntityInterface $entity, $related_field, Request $request) { - $related_field = $this->resourceType->getInternalName($related_field); - $this->relationshipAccess($entity, 'view', $related_field); + public function getRelated(FieldableEntityInterface $entity, $related_field, Request $request) { /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */ - $field_list = $entity->get($related_field); - $this->validateReferencedResource($field_list, $related_field); + $field_list = $entity->get($this->resourceType->getInternalName($related_field)); // Add the cacheable metadata from the host entity. $cacheable_metadata = CacheableMetadata::createFromObject($entity); $is_multiple = $field_list @@ -446,7 +443,7 @@ class EntityResource { /** * Gets the relationship of an entity. * - * @param \Drupal\Core\Entity\EntityInterface $entity + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity * The requested entity. * @param string $related_field * The related field name. @@ -458,41 +455,13 @@ class EntityResource { * @return \Drupal\jsonapi\ResourceResponse * The response. */ - public function getRelationship(EntityInterface $entity, $related_field, Request $request, $response_code = 200) { - $related_field = $this->resourceType->getInternalName($related_field); - $this->relationshipAccess($entity, 'view', $related_field); + public function getRelationship(FieldableEntityInterface $entity, $related_field, Request $request, $response_code = 200) { /* @var \Drupal\Core\Field\FieldItemListInterface $field_list */ - $field_list = $entity->get($related_field); - $this->validateReferencedResource($field_list, $related_field); + $field_list = $entity->get($this->resourceType->getInternalName($related_field)); $response = $this->buildWrappedResponse($field_list, $response_code); return $response; } - /** - * Validates that the referenced field points to an enabled resource. - * - * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface|null $field_list - * The field list with the reference. - * @param string $related_field - * The internal name of the related field. - * - * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException - * If the field is not a reference or the target resource is disabled. - * @throws \Symfony\Component\HttpKernel\Exception\HttpException - * If the $field_list is of the incorrect type. - */ - protected function validateReferencedResource($field_list, $related_field) { - if ( - !is_null($field_list) && - !$field_list instanceof EntityReferenceFieldItemListInterface - ) { - throw new HttpException(500, 'Invalid internal structure for relationship field list.'); - } - if (!$field_list || !$this->isRelationshipField($field_list)) { - throw new NotFoundHttpException(sprintf('The relationship %s is not present in this resource.', $related_field)); - } - } - /** * Adds a relationship to a to-many relationship. * @@ -518,7 +487,6 @@ class EntityResource { public function createRelationship(EntityInterface $entity, $related_field, $parsed_field_list, Request $request) { $related_field = $this->resourceType->getInternalName($related_field); /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $parsed_field_list */ - $this->relationshipAccess($entity, 'update', $related_field); if ($parsed_field_list instanceof Response) { // This usually means that there was an error, so there is no point on // processing further. @@ -619,7 +587,6 @@ class EntityResource { return $parsed_field_list; } /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $parsed_field_list */ - $this->relationshipAccess($entity, 'update', $related_field); // According to the specification, PATCH works a little bit different if the // relationship is to-one or to-many. /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */ @@ -708,8 +675,6 @@ class EntityResource { throw new BadRequestHttpException(sprintf('You need to provide a body for DELETE operations on a relationship (%s).', $related_field)); } /* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $parsed_field_list */ - $this->relationshipAccess($entity, 'update', $related_field); - $field_name = $parsed_field_list->getName(); $field_access = $parsed_field_list->access('edit', NULL, TRUE); if (!$field_access->isAllowed()) { diff --git a/tests/src/Functional/ResourceResponseTestTrait.php b/tests/src/Functional/ResourceResponseTestTrait.php index f9e74b7..d5e1cd3 100644 --- a/tests/src/Functional/ResourceResponseTestTrait.php +++ b/tests/src/Functional/ResourceResponseTestTrait.php @@ -465,8 +465,9 @@ trait ResourceResponseTestTrait { * for testing related/relationship routes and includes. * @param string|null $detail * (optional) Details for the JSON API error object. - * @param string|null $pointer - * (optional) Document pointer for the JSON API error object. + * @param string|bool|null $pointer + * (optional) Document pointer for the JSON API error object. FALSE to omit + * the pointer. * @param string|null $id * (optional) ID for the JSON API error object. * @@ -493,7 +494,7 @@ trait ResourceResponseTestTrait { if (!is_null($id)) { $error['id'] = $id; } - if ($relationship_field_name || $pointer) { + if ($pointer || $pointer !== FALSE && $relationship_field_name) { $error['source']['pointer'] = ($pointer) ? $pointer : $relationship_field_name; } return (new ResourceResponse(['errors' => [$error]], 403))->addCacheableDependency($access); diff --git a/tests/src/Functional/ResourceTestBase.php b/tests/src/Functional/ResourceTestBase.php index 79d7730..2f62ad6 100644 --- a/tests/src/Functional/ResourceTestBase.php +++ b/tests/src/Functional/ResourceTestBase.php @@ -1415,9 +1415,8 @@ abstract class ResourceTestBase extends BrowserTestBase { $relationship_field_name = 'field_jsonapi_test_entity_ref'; /* @var \Drupal\Core\Access\AccessResultReasonInterface $update_access */ $update_access = static::entityAccess($resource, 'update', $this->account) - ->andIf(static::entityFieldAccess($resource, $relationship_field_name, 'update', $this->account)); - $url = Url::fromRoute(sprintf("jsonapi.{$resource_identifier['type']}.relationship"), [ - 'related' => $relationship_field_name, + ->andIf(static::entityFieldAccess($resource, $relationship_field_name, 'edit', $this->account)); + $url = Url::fromRoute(sprintf("jsonapi.{$resource_identifier['type']}.{$relationship_field_name}.relationship"), [ $resource->getEntityTypeId() => $resource->uuid(), ]); @@ -1571,13 +1570,13 @@ abstract class ResourceTestBase extends BrowserTestBase { else { $request_options[RequestOptions::BODY] = Json::encode(['data' => [$target_identifier]]); $response = $this->request('POST', $url, $request_options); - $message = 'The current user is not allowed to update this relationship.'; + $message = 'The current user is not allowed to edit this relationship.'; $message .= ($reason = $update_access->getReason()) ? ' ' . $reason : ''; - $this->assertResourceErrorResponse(403, $message, $response, $relationship_field_name); + $this->assertResourceErrorResponse(403, $message, $response); $response = $this->request('PATCH', $url, $request_options); - $this->assertResourceErrorResponse(403, $message, $response, $relationship_field_name); + $this->assertResourceErrorResponse(403, $message, $response); $response = $this->request('DELETE', $url, $request_options); - $this->assertResourceErrorResponse(403, $message, $response, $relationship_field_name); + $this->assertResourceErrorResponse(403, $message, $response); } // Remove the test entities that were created. @@ -1600,7 +1599,7 @@ abstract class ResourceTestBase extends BrowserTestBase { $entity = $entity ?: $this->entity; $access = static::entityFieldAccess($entity, $relationship_field_name, 'view', $this->account); if (!$access->isAllowed()) { - return static::getAccessDeniedResponse($this->entity, $access, $relationship_field_name, 'The current user is not allowed to view this relationship.'); + return static::getAccessDeniedResponse($this->entity, $access, $relationship_field_name, 'The current user is not allowed to view this relationship.', FALSE); } $expected_document = $this->getExpectedGetRelationshipDocument($relationship_field_name); $status_code = isset($expected_document['errors'][0]['status']) ? $expected_document['errors'][0]['status'] : 200; @@ -1719,9 +1718,6 @@ abstract class ResourceTestBase extends BrowserTestBase { ], 'code' => 0, 'id' => '/' . $base_resource_identifier['type'] . '/' . $base_resource_identifier['id'], - 'source' => [ - 'pointer' => $relationship_field_name, - ], ], ], ], 403))->addCacheableDependency($access); @@ -2762,7 +2758,7 @@ abstract class ResourceTestBase extends BrowserTestBase { * The AccessResult. */ protected static function entityFieldAccess(EntityInterface $entity, $field_name, $operation, AccountInterface $account) { - $entity_access = static::entityAccess($entity, $operation, $account); + $entity_access = static::entityAccess($entity, $operation === 'edit' ? 'update' : 'view', $account); $field_access = $entity->{$field_name}->access($operation, $account, TRUE); return $entity_access->andIf($field_access); }