diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index 2c6dbc1..d4e203d 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -527,10 +527,8 @@ protected function invalidateTagsOnSave($update) { // listing's filtering requirements. A newly created entity may start to // appear in listings because it did not exist before.) $tags = $this->getEntityType()->getListCacheTags(); - if ($this->hasLinkTemplate('canonical')) { - // Creating or updating an entity may change a cached 403 or 404 response. - $tags = Cache::mergeTags($tags, ['4xx-response']); - } + // Creating or updating an entity may change a cached 403 or 404 response. + $tags = Cache::mergeTags($tags, ['4xx-response']); if ($update) { // An existing entity was updated, also invalidate its unique cache tag. $tags = Cache::mergeTags($tags, $this->getCacheTagsToInvalidate()); diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php index 2853b98..1302a47 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php @@ -2,7 +2,7 @@ namespace Drupal\Core\EventSubscriber; -use Symfony\Component\HttpFoundation\JsonResponse; +use Drupal\Core\Cache\CacheableJsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; @@ -34,7 +34,7 @@ protected static function getPriority() { * The event to process. */ public function on400(GetResponseForExceptionEvent $event) { - $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_BAD_REQUEST); + $response = new CacheableJsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_BAD_REQUEST); $event->setResponse($response); } @@ -45,7 +45,7 @@ public function on400(GetResponseForExceptionEvent $event) { * The event to process. */ public function on403(GetResponseForExceptionEvent $event) { - $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_FORBIDDEN); + $response = new CacheableJsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_FORBIDDEN); $event->setResponse($response); } @@ -56,7 +56,7 @@ public function on403(GetResponseForExceptionEvent $event) { * The event to process. */ public function on404(GetResponseForExceptionEvent $event) { - $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_NOT_FOUND); + $response = new CacheableJsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_NOT_FOUND); $event->setResponse($response); } @@ -67,7 +67,7 @@ public function on404(GetResponseForExceptionEvent $event) { * The event to process. */ public function on405(GetResponseForExceptionEvent $event) { - $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_METHOD_NOT_ALLOWED); + $response = new CacheableJsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_METHOD_NOT_ALLOWED); $event->setResponse($response); } @@ -78,7 +78,7 @@ public function on405(GetResponseForExceptionEvent $event) { * The event to process. */ public function on406(GetResponseForExceptionEvent $event) { - $response = new JsonResponse(['message' => $event->getException()->getMessage()], Response::HTTP_NOT_ACCEPTABLE); + $response = new CacheableJsonResponse(['message' => $event->getException()->getMessage()], Response::HTTP_NOT_ACCEPTABLE); $event->setResponse($response); } @@ -89,7 +89,7 @@ public function on406(GetResponseForExceptionEvent $event) { * The event to process. */ public function on415(GetResponseForExceptionEvent $event) { - $response = new JsonResponse(['message' => $event->getException()->getMessage()], Response::HTTP_UNSUPPORTED_MEDIA_TYPE); + $response = new CacheableJsonResponse(['message' => $event->getException()->getMessage()], Response::HTTP_UNSUPPORTED_MEDIA_TYPE); $event->setResponse($response); } diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index dfb0272..a46552b 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -37,7 +37,6 @@ class EntityResource extends ResourceBase implements DependentPluginInterface { use EntityResourceValidationTrait; - use EntityResourceAccessTrait; /** * The entity type targeted by this resource. @@ -106,7 +105,10 @@ public static function create(ContainerInterface $container, array $configuratio public function get(EntityInterface $entity) { $entity_access = $entity->access('view', NULL, TRUE); if (!$entity_access->isAllowed()) { - throw new AccessDeniedHttpException(); + $response = new ResourceResponse(NULL, 403); + $response->addCacheableDependency($entity); + $response->addCacheableDependency($entity_access); + return $response; } $response = new ResourceResponse($entity, 200); @@ -144,8 +146,11 @@ public function post(EntityInterface $entity = NULL) { throw new BadRequestHttpException('No entity content received.'); } - if (!$entity->access('create')) { - throw new AccessDeniedHttpException(); + $entity_access = $entity->access('create', NULL, TRUE); + if (!$entity_access->isAllowed()) { + $response = new ResourceResponse(NULL, 403); + $response->addCacheableDependency($entity_access); + return $response; } $definition = $this->getPluginDefinition(); // Verify that the deserialized entity is of the type that we expect to @@ -159,7 +164,20 @@ public function post(EntityInterface $entity = NULL) { throw new BadRequestHttpException('Only new entities can be created'); } - $this->checkEditFieldAccess($entity); + // Only check 'edit' permissions for fields that were actually + // submitted by the user. Field access makes no difference between 'create' + // and 'update', so the 'edit' operation is used here. + foreach ($entity->_restSubmittedFields as $key => $field_name) { + $field_access = $entity->get($field_name)->access('edit', NULL, TRUE); + if (!$field_access->isAllowed()) { + $response = new ResourceResponse([ + 'message' => "Access denied on creating field '$field_name'" + ], 403); + $response->addCacheableDependency($entity_access); + $response->addCacheableDependency($field_access); + return $response; + } + } // Validate the received data before saving. $this->validate($entity); @@ -199,8 +217,12 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity if ($entity->getEntityTypeId() != $definition['entity_type']) { throw new BadRequestHttpException('Invalid entity type'); } - if (!$original_entity->access('update')) { - throw new AccessDeniedHttpException(); + $original_entity_access = $original_entity->access('update', NULL, TRUE); + if (!$original_entity_access->isAllowed()) { + $response = new ResourceResponse(NULL, 403); + $response->addCacheableDependency($original_entity); + $response->addCacheableDependency($original_entity_access); + return $response; } // Overwrite the received properties. @@ -224,8 +246,15 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity } } - if (!$original_entity->get($field_name)->access('edit')) { - throw new AccessDeniedHttpException("Access denied on updating field '$field_name'."); + $field_access = $original_entity->get($field_name)->access('edit', NULL, TRUE); + if (!$field_access->isAllowed()) { + $response = new ResourceResponse([ + 'message' => "Access denied on updating field '$field_name'." + ], 403); + $response->addCacheableDependency($original_entity); + $response->addCacheableDependency($original_entity_access); + $response->addCacheableDependency($field_access); + return $response; } $original_entity->set($field_name, $field->getValue()); } @@ -256,8 +285,12 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ public function delete(EntityInterface $entity) { - if (!$entity->access('delete')) { - throw new AccessDeniedHttpException(); + $entity_access = $entity->access('delete', NULL, TRUE); + if (!$entity_access->isAllowed()) { + $response = new ResourceResponse(NULL, 403); + $response->addCacheableDependency($entity); + $response->addCacheableDependency($entity_access); + return $response; } try { $entity->delete(); diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php deleted file mode 100644 index 7bf8e82..0000000 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php +++ /dev/null @@ -1,35 +0,0 @@ -_restSubmittedFields as $key => $field_name) { - if (!$entity->get($field_name)->access('edit')) { - throw new AccessDeniedHttpException("Access denied on creating field '$field_name'."); - } - } - } - -} diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php index 088dca2..1ce98ea 100644 --- a/core/modules/rest/src/RequestHandler.php +++ b/core/modules/rest/src/RequestHandler.php @@ -2,6 +2,7 @@ namespace Drupal\rest; +use Drupal\Core\Cache\CacheableResponse; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Cache\CacheableResponseInterface; @@ -12,6 +13,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\SerializerInterface; @@ -107,9 +110,7 @@ public function handle(RouteMatchInterface $route_match, Request $request) { } } catch (UnexpectedValueException $e) { - $error['error'] = $e->getMessage(); - $content = $serializer->serialize($error, $format); - return new Response($content, 400, array('Content-Type' => $request->getMimeType($format))); + throw new BadRequestHttpException($e->getMessage()); } } else { diff --git a/core/modules/rest/src/Tests/NodeTest.php b/core/modules/rest/src/Tests/NodeTest.php index 95dc475..e14b5ac 100644 --- a/core/modules/rest/src/Tests/NodeTest.php +++ b/core/modules/rest/src/Tests/NodeTest.php @@ -174,7 +174,7 @@ public function testInvalidBundle() { // Make sure the response is "Bad Request". $this->assertResponse(400); - $this->assertResponseBody('{"error":"\"bad_bundle_name\" is not a valid bundle type for denormalization."}'); + $this->assertResponseBody('{"message":"\"bad_bundle_name\" is not a valid bundle type for denormalization."}'); } /** @@ -191,7 +191,7 @@ public function testMissingBundle() { // Make sure the response is "Bad Request". $this->assertResponse(400); - $this->assertResponseBody('{"error":"A string must be provided as a bundle value."}'); + $this->assertResponseBody('{"message":"A string must be provided as a bundle value."}'); } } diff --git a/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php b/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php index ba91836..894776d 100644 --- a/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php +++ b/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php @@ -2,6 +2,7 @@ namespace Drupal\serialization\EventSubscriber; +use Drupal\Core\Cache\CacheableResponse; use Drupal\Core\EventSubscriber\HttpExceptionSubscriberBase; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; @@ -137,7 +138,7 @@ protected function setEventResponse(GetResponseForExceptionEvent $event, $status $format = $event->getRequest()->getRequestFormat(); $content = ['message' => $event->getException()->getMessage()]; $encoded_content = $this->serializer->serialize($content, $format); - $response = new Response($encoded_content, $status); + $response = new CacheableResponse($encoded_content, $status); $event->setResponse($response); } diff --git a/core/modules/user/src/Plugin/rest/resource/UserRegistrationResource.php b/core/modules/user/src/Plugin/rest/resource/UserRegistrationResource.php index 6a243c3..53d084d 100644 --- a/core/modules/user/src/Plugin/rest/resource/UserRegistrationResource.php +++ b/core/modules/user/src/Plugin/rest/resource/UserRegistrationResource.php @@ -6,7 +6,6 @@ use Drupal\Core\Session\AccountInterface; use Drupal\rest\ModifiedResourceResponse; use Drupal\rest\Plugin\ResourceBase; -use Drupal\rest\Plugin\rest\resource\EntityResourceAccessTrait; use Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait; use Drupal\user\UserInterface; use Psr\Log\LoggerInterface; @@ -30,7 +29,6 @@ class UserRegistrationResource extends ResourceBase { use EntityResourceValidationTrait; - use EntityResourceAccessTrait; /** * User settings config instance. @@ -109,7 +107,20 @@ public function post(UserInterface $account = NULL) { $account->block(); } - $this->checkEditFieldAccess($account); + // Only check 'edit' permissions for fields that were actually + // submitted by the user. Field access makes no difference between 'create' + // and 'update', so the 'edit' operation is used here. + foreach ($entity->_restSubmittedFields as $key => $field_name) { + $field_access = $entity->get($field_name)->access('edit', NULL, TRUE); + if (!$field_access->isAllowed()) { + $response = new ResourceResponse([ + 'error' => "Access denied on creating field '$field_name'" + ], 403); + $response->addCacheableDependency($entity_access); + $response->addCacheableDependency($field_access); + return $response; + } + } // Make sure that the user entity is valid (email and name are valid). $this->validate($account); diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php index f150e4fe..407e75d 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityUnitTest.php @@ -392,11 +392,13 @@ public function testPostSave() { $this->cacheTagsInvalidator->expects($this->at(0)) ->method('invalidateTags') ->with(array( + '4xx-response', $this->entityTypeId . '_list', // List cache tag. )); $this->cacheTagsInvalidator->expects($this->at(1)) ->method('invalidateTags') ->with(array( + '4xx-response', $this->entityTypeId . ':' . $this->values['id'], // Own cache tag. $this->entityTypeId . '_list', // List cache tag. ));