Commit 701be771 authored by alexpott's avatar alexpott

Issue #2869426 by Wim Leers, dawehner, tstoeckler, alexpott, Berdir, larowlan:...

Issue #2869426 by Wim Leers, dawehner, tstoeckler, alexpott, Berdir, larowlan: EntityResource should add _entity_access requirement to REST routes
parent ae7a94a3
......@@ -89,12 +89,15 @@ public function applies(Route $route) {
public function access(Request $request, AccountInterface $account) {
$method = $request->getMethod();
// Read-only operations are always allowed.
if (in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'], TRUE)) {
return AccessResult::allowed();
}
// This check only applies if
// 1. this is a write operation
// 2. the user was successfully authenticated and
// 3. the request comes with a session cookie.
if (!in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'])
&& $account->isAuthenticated()
// 1. the user was successfully authenticated and
// 2. the request comes with a session cookie.
if ($account->isAuthenticated()
&& $this->sessionConfiguration->hasSession($request)
) {
if (!$request->headers->has('X-CSRF-Token')) {
......
......@@ -123,6 +123,21 @@ public function onExceptionSendChallenge(GetResponseForExceptionEvent $event) {
}
}
/**
* Detect disallowed authentication methods on access denied exceptions.
*
* @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
*/
public function onExceptionAccessDenied(GetResponseForExceptionEvent $event) {
if (isset($this->filter) && $event->isMasterRequest()) {
$request = $event->getRequest();
$exception = $event->getException();
if ($exception instanceof AccessDeniedHttpException && $this->authenticationProvider->applies($request) && !$this->filter->appliesToRoutedRequest($request, TRUE)) {
$event->setException(new AccessDeniedHttpException('The used authentication method is not allowed on this route.', $exception));
}
}
}
/**
* {@inheritdoc}
*/
......@@ -136,6 +151,7 @@ public static function getSubscribedEvents() {
// Access check must be performed after routing.
$events[KernelEvents::REQUEST][] = ['onKernelRequestFilterProvider', 31];
$events[KernelEvents::EXCEPTION][] = ['onExceptionSendChallenge', 75];
$events[KernelEvents::EXCEPTION][] = ['onExceptionAccessDenied', 80];
return $events;
}
......
......@@ -4,6 +4,8 @@
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
......@@ -111,7 +113,12 @@ protected function checkAccess(Request $request) {
$request->attributes->set(AccessAwareRouterInterface::ACCESS_RESULT, $access_result);
}
if (!$access_result->isAllowed()) {
throw new AccessDeniedHttpException($access_result instanceof AccessResultReasonInterface ? $access_result->getReason() : NULL);
if ($access_result instanceof CacheableDependencyInterface && $request->isMethodCacheable()) {
throw new CacheableAccessDeniedHttpException($access_result, $access_result instanceof AccessResultReasonInterface ? $access_result->getReason() : NULL);
}
else {
throw new AccessDeniedHttpException($access_result instanceof AccessResultReasonInterface ? $access_result->getReason() : NULL);
}
}
}
......
......@@ -12,6 +12,7 @@
use Drupal\Core\Http\Exception\CacheableUnauthorizedHttpException;
use Drupal\user\UserAuthInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
/**
* HTTP Basic authentication provider.
......@@ -155,7 +156,9 @@ public function challengeException(Request $request, \Exception $previous) {
$cacheability = CacheableMetadata::createFromObject($site_config)
->addCacheTags(['config:user.role.anonymous'])
->addCacheContexts(['user.roles:anonymous']);
return new CacheableUnauthorizedHttpException($cacheability, (string) $challenge, 'No authentication credentials provided.', $previous);
return $request->isMethodCacheable()
? new CacheableUnauthorizedHttpException($cacheability, (string) $challenge, 'No authentication credentials provided.', $previous)
: new UnauthorizedHttpException((string) $challenge, 'No authentication credentials provided.', $previous);
}
}
......@@ -136,7 +136,10 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
}
}
else {
$access = AccessResult::forbidden();
$reason = count($conditions) > 1
? "One of the block visibility conditions ('%s') denied access."
: "The block visibility condition '%s' denied access.";
$access = AccessResult::forbidden(sprintf($reason, implode("', '", array_keys($conditions))));
}
$this->mergeCacheabilityFromConditions($access, $conditions);
......
......@@ -3,6 +3,7 @@
namespace Drupal\Tests\block\Functional\Rest;
use Drupal\block\Entity\Block;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
abstract class BlockResourceTestBase extends EntityResourceTestBase {
......@@ -135,7 +136,7 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
switch ($method) {
case 'GET':
return "You are not authorized to view this block entity.";
return "The block visibility condition 'user_role' denied access.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
......@@ -143,17 +144,25 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
/**
* {@inheritdoc}
*
* @todo Fix this in https://www.drupal.org/node/2820315.
*/
protected function getExpectedUnauthorizedAccessCacheability() {
return (new CacheableMetadata())
->setCacheTags(['4xx-response', 'http_response'])
->setCacheContexts(['user.roles']);
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\block\BlockAccessControlHandler::checkAccess()
return parent::getExpectedUnauthorizedAccessCacheability()
->setCacheTags([
'4xx-response',
return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
->addCacheTags([
'config:block.block.llama',
'http_response',
static::$auth ? 'user:2' : 'user:0',
])
->setCacheContexts(['user.roles']);
$is_authenticated ? 'user:2' : 'user:0',
]);
}
}
......@@ -180,9 +180,9 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessCacheability() {
protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\block_content\BlockContentAccessControlHandler()
return parent::getExpectedUnauthorizedAccessCacheability()
return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
->addCacheTags(['block_content:1']);
}
......
......@@ -319,8 +319,10 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
return "The 'post comments' permission is required.";
case 'PATCH';
return "The 'edit own comments' permission is required, the user must be the comment author, and the comment must be published.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
case 'DELETE':
// \Drupal\comment\CommentAccessControlHandler::checkAccess() does not
// specify a reason for not allowing a comment to be deleted.
return '';
}
}
......@@ -360,9 +362,9 @@ public function testPostSkipCommentApproval() {
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessCacheability() {
protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\comment\CommentAccessControlHandler::checkAccess()
return parent::getExpectedUnauthorizedAccessCacheability()
return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
->addCacheTags(['comment:1']);
}
......
......@@ -70,4 +70,20 @@ protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
switch ($method) {
case 'GET':
return "The 'view config_test' permission is required.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
}
......@@ -67,7 +67,7 @@ public function testWatchdog() {
$request_options = $this->getAuthenticationRequestOptions('GET');
$response = $this->request('GET', $url, $request_options);
$this->assertResourceErrorResponse(403, "The 'restful get dblog' permission is required.", $response);
$this->assertResourceErrorResponse(403, "The 'restful get dblog' permission is required.", $response, ['4xx-response', 'http_response'], ['user.permissions'], FALSE, FALSE);
// Create a user account that has the required permissions to read
// the watchdog resource via the REST API.
......
......@@ -218,6 +218,9 @@ public function testPost() {
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
if ($method === 'DELETE') {
return 'Only the file owner can update or delete the file entity.';
}
return parent::getExpectedUnauthorizedAccessMessage($method);
}
......
......@@ -3,14 +3,14 @@
namespace Drupal\Tests\language\Functional\Hal;
use Drupal\Tests\language\Functional\Rest\ConfigurableLanguageResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class ConfigurableLanguageHalJsonBasicAuthTest extends ConfigurableLanguageResourceTestBase {
use BasicAuthResourceWithInterfaceTranslationTestTrait;
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
......
......@@ -3,14 +3,14 @@
namespace Drupal\Tests\language\Functional\Hal;
use Drupal\Tests\language\Functional\Rest\ContentLanguageSettingsResourceTestBase;
use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class ContentLanguageSettingsHalJsonBasicAuthTest extends ContentLanguageSettingsResourceTestBase {
use BasicAuthResourceWithInterfaceTranslationTestTrait;
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
......
......@@ -2,14 +2,14 @@
namespace Drupal\Tests\language\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class ConfigurableLanguageJsonBasicAuthTest extends ConfigurableLanguageResourceTestBase {
use BasicAuthResourceWithInterfaceTranslationTestTrait;
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
......
......@@ -2,7 +2,7 @@
namespace Drupal\Tests\language\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
......@@ -10,7 +10,7 @@
*/
class ConfigurableLanguageXmlBasicAuthTest extends ConfigurableLanguageResourceTestBase {
use BasicAuthResourceWithInterfaceTranslationTestTrait;
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
......
......@@ -2,14 +2,14 @@
namespace Drupal\Tests\language\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class ContentLanguageSettingsJsonBasicAuthTest extends ContentLanguageSettingsResourceTestBase {
use BasicAuthResourceWithInterfaceTranslationTestTrait;
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
......
......@@ -2,7 +2,7 @@
namespace Drupal\Tests\language\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
......@@ -10,7 +10,7 @@
*/
class ContentLanguageSettingsXmlBasicAuthTest extends ContentLanguageSettingsResourceTestBase {
use BasicAuthResourceWithInterfaceTranslationTestTrait;
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
......
......@@ -436,9 +436,9 @@ protected function getExpectedNormalizedFileEntity() {
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessCacheability() {
protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
// @see \Drupal\media\MediaAccessControlHandler::checkAccess()
return parent::getExpectedUnauthorizedAccessCacheability()
return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated)
->addCacheTags(['media:1']);
}
......
......@@ -72,7 +72,8 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
}
case 'delete':
return AccessResult::allowedIf(!$entity->isNew() && $account->hasPermission('administer menu'))->cachePerPermissions()->addCacheableDependency($entity);
return AccessResult::allowedIfHasPermission($account, 'administer menu')
->andIf(AccessResult::allowedIf(!$entity->isNew())->addCacheableDependency($entity));
}
}
......
......@@ -204,7 +204,7 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
switch ($method) {
case 'DELETE':
return "You are not authorized to delete this menu_link_content entity.";
return "The 'administer menu' permission is required.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
......
......@@ -13,12 +13,13 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException;
use Drupal\Core\Routing\AccessAwareRouterInterface;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\rest\ModifiedResourceResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
......@@ -120,15 +121,11 @@ public static function create(ContainerInterface $container, array $configuratio
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function get(EntityInterface $entity) {
$entity_access = $entity->access('view', NULL, TRUE);
if (!$entity_access->isAllowed()) {
throw new CacheableAccessDeniedHttpException($entity_access, $entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view'));
}
public function get(EntityInterface $entity, Request $request) {
$response = new ResourceResponse($entity, 200);
// @todo Either remove the line below or remove this todo in https://www.drupal.org/project/drupal/issues/2973356
$response->addCacheableDependency($request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT));
$response->addCacheableDependency($entity);
$response->addCacheableDependency($entity_access);
if ($entity instanceof FieldableEntityInterface) {
foreach ($entity as $field_name => $field) {
......@@ -223,10 +220,6 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
if ($entity->getEntityTypeId() != $definition['entity_type']) {
throw new BadRequestHttpException('Invalid entity type');
}
$entity_access = $original_entity->access('update', NULL, TRUE);
if (!$entity_access->isAllowed()) {
throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'update'));
}
// Overwrite the received fields.
// @todo Remove $changed_fields in https://www.drupal.org/project/drupal/issues/2862574.
......@@ -327,10 +320,6 @@ protected function checkPatchFieldAccess(FieldItemListInterface $original_field,
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function delete(EntityInterface $entity) {
$entity_access = $entity->access('delete', NULL, TRUE);
if (!$entity_access->isAllowed()) {
throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'delete'));
}
try {
$entity->delete();
$this->logger->notice('Deleted entity %type with ID %id.', ['%type' => $entity->getEntityTypeId(), '%id' => $entity->id()]);
......@@ -383,6 +372,19 @@ public function permissions() {
*/
protected function getBaseRoute($canonical_path, $method) {
$route = parent::getBaseRoute($canonical_path, $method);
switch ($method) {
case 'GET':
$route->setRequirement('_entity_access', $this->entityType->id() . '.view');
break;
case 'PATCH':
$route->setRequirement('_entity_access', $this->entityType->id() . '.update');
break;
case 'DELETE':
$route->setRequirement('_entity_access', $this->entityType->id() . '.delete');
break;
}
$definition = $this->getPluginDefinition();
$parameters = $route->getOption('parameters') ?: [];
......
......@@ -6,6 +6,7 @@
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
......@@ -26,5 +27,9 @@ function config_test_rest_config_test_access(EntityInterface $entity, $operation
// Add permission, so that EntityResourceTestBase's scenarios can test access
// being denied. By default, all access is always allowed for the config_test
// config entity.
return AccessResult::forbiddenIf(!$account->hasPermission('view config_test'))->cachePerPermissions();
$access_result = AccessResult::forbiddenIf(!$account->hasPermission('view config_test'))->cachePerPermissions();
if (!$access_result->isAllowed() && $access_result instanceof AccessResultReasonInterface) {
$access_result->setReason("The 'view config_test' permission is required.");
}
return $access_result;
}
......@@ -14,8 +14,6 @@
* authenticated, a 401 response must be sent.
* - Because every request must send an authorization, there is no danger of
* CSRF attacks.
*
* @see \Drupal\Tests\rest\Functional\BasicAuthResourceWithInterfaceTranslationTestTrait
*/
trait BasicAuthResourceTestTrait {
......@@ -34,10 +32,23 @@ protected function getAuthenticationRequestOptions($method) {
* {@inheritdoc}
*/
protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
if ($method !== 'GET') {
return $this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response);
}
$expected_page_cache_header_value = $method === 'GET' ? 'MISS' : FALSE;
// @see \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
$expected_dynamic_page_cache_header_value = $expected_page_cache_header_value;
$this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, ['4xx-response', 'config:system.site', 'config:user.role.anonymous', 'http_response'], ['user.roles:anonymous'], $expected_page_cache_header_value, $expected_dynamic_page_cache_header_value);
$expected_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(FALSE))
// @see \Drupal\basic_auth\Authentication\Provider\BasicAuth::challengeException()
->addCacheableDependency($this->config('system.site'))
// @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
->addCacheTags(['config:user.role.anonymous']);
// Only add the 'user.roles:anonymous' cache context if its parent cache
// context is not already present.
if (!in_array('user.roles', $expected_cacheability->getCacheContexts(), TRUE)) {
$expected_cacheability->addCacheContexts(['user.roles:anonymous']);
}
$this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), $expected_page_cache_header_value, FALSE);
}
/**
......
<?php
namespace Drupal\Tests\rest\Functional;
use Psr\Http\Message\ResponseInterface;
/**
* Trait for ResourceTestBase subclasses testing $auth=basic_auth + 'language'.
*
* @see \Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait
*/
trait BasicAuthResourceWithInterfaceTranslationTestTrait {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {
// Because BasicAuth::challengeException() relies on the 'system.site'
// configuration, and this test installs the 'language' module, all config
// may be translated and therefore gets the 'languages:language_interface'
// cache context.
$expected_page_cache_header_value = $method === 'GET' ? 'MISS' : FALSE;
$this->assertResourceErrorResponse(401, 'No authentication credentials provided.', $response, ['4xx-response', 'config:system.site', 'config:user.role.anonymous', 'http_response'], ['languages:language_interface', 'user.roles:anonymous'], $expected_page_cache_header_value, $expected_page_cache_header_value);
}
}
......@@ -99,7 +99,9 @@ protected function assertResponseWhenMissingAuthentication($method, ResponseInte
// @see \Drupal\user\Authentication\Provider\Cookie
// @todo https://www.drupal.org/node/2847623
if ($method === 'GET') {
$expected_cookie_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability();
$expected_cookie_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
// @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(FALSE));
// - \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber applies
// to cacheable anonymous responses: it updates their cacheability.
// - A 403 response to a GET request is cacheable.
......@@ -111,7 +113,7 @@ protected function assertResponseWhenMissingAuthentication($method, ResponseInte
if (static::$entityTypeId === 'block') {
$expected_cookie_403_cacheability->setCacheTags(str_replace('user:2', 'user:0', $expected_cookie_403_cacheability->getCacheTags()));
}
$this->assertResourceErrorResponse(403, FALSE, $response, $expected_cookie_403_cacheability->getCacheTags(), $expected_cookie_403_cacheability->getCacheContexts(), 'MISS', 'MISS');
$this->assertResourceErrorResponse(403, FALSE, $response, $expected_cookie_403_cacheability->getCacheTags(), $expected_cookie_403_cacheability->getCacheContexts(), 'MISS', FALSE);
}
else {
$this->assertResourceErrorResponse(403, FALSE, $response);
......
......@@ -379,6 +379,20 @@ protected function getExpectedUnauthorizedAccessCacheability() {
->setCacheContexts(['user.permissions']);
}
/**
* The cacheability of unauthorized 'view' entity access.
*
* @param bool $is_authenticated
* Whether the current request is authenticated or not. This matters for
* some entity access control handlers, but not for most.
*
* @return \Drupal\Core\Cache\CacheableMetadata
* The expected cacheability.
*/
protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) {
return new CacheableMetadata();
}
/**
* The expected cache tags for the GET/HEAD response of the test entity.
*
......@@ -441,7 +455,11 @@ public function testGet() {
// response because ?_format query string is present.
$response = $this->request('GET', $url, $request_options);
if ($has_canonical_url) {
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response);
$expected_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
// @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
->addCacheTags(['config:user.role.anonymous']);
$expected_cacheability->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(FALSE));
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_cacheability->getCacheTags(), $expected_cacheability->getCacheContexts(), 'MISS', FALSE);
}
else {
$this->assertResourceErrorResponse(404, 'No route found for "GET ' . str_replace($this->baseUrl, '', $this->getEntityResourceUrl()->setAbsolute()->toString()) . '"', $response);
......@@ -474,7 +492,8 @@ public function testGet() {
// First: single format. Drupal will automatically pick the only format.
$this->provisionEntityResource(TRUE);
$expected_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability();
$expected_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability()
->addCacheableDependency($this->getExpectedUnauthorizedEntityAccessCacheability(static::$auth !== FALSE));
// DX: 403 because unauthorized single-format route, ?_format is omittable.
$url->setOption('query', []);
$response = $this->request('GET', $url, $request_options);
......@@ -483,13 +502,13 @@ public function testGet() {
$this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type'));
}
else {
$this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', 'MISS');
$this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
}
$this->assertSame(static::$auth ? [] : ['MISS'], $response->getHeader('X-Drupal-Cache'));
// DX: 403 because unauthorized.
$url->setOption('query', ['_format' => static::$format]);
$response = $this->request('GET', $url, $request_options);
$this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', $has_canonical_url ? 'MISS' : 'HIT');
$this->assertResourceErrorResponse(403, FALSE, $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
// Then, what we'll use for the remainder of the test: multiple formats.
$this->provisionEntityResource();
......@@ -509,7 +528,7 @@ public function testGet() {
// DX: 403 because unauthorized.
$url->setOption('query', ['_format' => static::$format]);
$response = $this->request('GET', $url, $request_options);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', 'HIT');
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
$this->assertArrayNotHasKey('Link', $response->getHeaders());
$this->setUpAuthorization('GET');
......@@ -687,7 +706,15 @@ public function testGet() {
// DX: 403 when unauthorized.
$response = $this->request('GET', $url, $request_options);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response);
$expected_403_cacheability = $this->getExpectedUnauthorizedAccessCacheability();
// Permission checking now happens first, so it's the only cache context we
// could possibly vary by.
$expected_403_cacheability->setCacheContexts(['user.permissions']);
// @see \Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber::onRespond()
if (static::$auth === FALSE) {
$expected_403_cacheability->addCacheTags(['config:user.role.anonymous']);
}
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response, $expected_403_cacheability->getCacheTags(), $expected_403_cacheability->getCacheContexts(), static::$auth ? FALSE : 'MISS', FALSE);
$this->grantPermissionsToTestedRole(['restful get entity:' . static::$entityTypeId]);
......@@ -1074,18 +1101,6 @@ public function testPatch() {
$request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
// DX: 400 when no request body.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceErrorResponse(400, 'No entity content received.', $response);
$request_options[RequestOptions::BODY] = $unparseable_request_body;
// DX: 400 when unparseable request body.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceErrorResponse(400, 'Syntax error', $response);
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body;
if (static::$auth) {
// DX: forgetting authentication: authentication provider-specific error
// response.
......@@ -1101,6 +1116,18 @@ public function testPatch() {
$this->setUpAuthorization('PATCH');
// DX: 400 when no request body.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceErrorResponse(400, 'No entity content received.', $response);
$request_options[RequestOptions::BODY] = $unparseable_request_body;
// DX: 400 when unparseable request body.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceErrorResponse(400, 'Syntax error', $response);