Commit 1fcd3d5c authored by alexpott's avatar alexpott

Issue #2808233 by gnuget, dawehner, Wim Leers, tedbow, Chi, dysrama: REST 403...

Issue #2808233 by gnuget, dawehner, Wim Leers, tedbow, Chi, dysrama: REST 403 responses don't tell the user *why* access is not granted: requires deep Drupal understanding to figure out
parent b52ac5eb
......@@ -31,17 +31,22 @@ abstract class AccessResult implements AccessResultInterface, RefinableCacheable
/**
* Creates an AccessResultInterface object with isNeutral() === TRUE.
*
* @return \Drupal\Core\Access\AccessResult
* @param string|null $reason
* (optional) The reason why access is forbidden. Intended for developers,
* hence not translatable.
*
* @return \Drupal\Core\Access\AccessResultNeutral
* isNeutral() will be TRUE.
*/
public static function neutral() {
return new AccessResultNeutral();
public static function neutral($reason = NULL) {
assert('is_string($reason) || is_null($reason)');
return new AccessResultNeutral($reason);
}
/**
* Creates an AccessResultInterface object with isAllowed() === TRUE.
*
* @return \Drupal\Core\Access\AccessResult
* @return \Drupal\Core\Access\AccessResultAllowed
* isAllowed() will be TRUE.
*/
public static function allowed() {
......@@ -55,7 +60,7 @@ public static function allowed() {
* (optional) The reason why access is forbidden. Intended for developers,
* hence not translatable.
*
* @return \Drupal\Core\Access\AccessResult
* @return \Drupal\Core\Access\AccessResultForbidden
* isForbidden() will be TRUE.
*/
public static function forbidden($reason = NULL) {
......@@ -106,7 +111,12 @@ public static function forbiddenIf($condition) {
* isNeutral() will be TRUE.
*/
public static function allowedIfHasPermission(AccountInterface $account, $permission) {
return static::allowedIf($account->hasPermission($permission))->addCacheContexts(['user.permissions']);
$access_result = static::allowedIf($account->hasPermission($permission))->addCacheContexts(['user.permissions']);
if ($access_result instanceof AccessResultReasonInterface) {
$access_result->setReason("The '$permission' permission is required.");
}
return $access_result;
}
/**
......@@ -147,7 +157,21 @@ public static function allowedIfHasPermissions(AccountInterface $account, array
}
}
return static::allowedIf($access)->addCacheContexts(empty($permissions) ? [] : ['user.permissions']);
$access_result = static::allowedIf($access)->addCacheContexts(empty($permissions) ? [] : ['user.permissions']);
if ($access_result instanceof AccessResultReasonInterface) {
if (count($permissions) === 1) {
$access_result->setReason("The '$permission' permission is required.");
}
elseif (count($permissions) > 1) {
$quote = function ($s) {
return "'$s'";
};
$access_result->setReason(sprintf("The following permissions are required: %s.", implode(" $conjunction ", array_map($quote, $permissions))));
}
}
return $access_result;
}
/**
......@@ -308,6 +332,13 @@ public function orIf(AccessResultInterface $other) {
if (!$this->isForbidden() || ($this->getCacheMaxAge() === 0 && $other->isForbidden())) {
$merge_other = TRUE;
}
if ($this->isForbidden() && $this instanceof AccessResultReasonInterface) {
$result->setReason($this->getReason());
}
elseif ($other->isForbidden() && $other instanceof AccessResultReasonInterface) {
$result->setReason($other->getReason());
}
}
elseif ($this->isAllowed() || $other->isAllowed()) {
$result = static::allowed();
......@@ -319,6 +350,14 @@ public function orIf(AccessResultInterface $other) {
$result = static::neutral();
if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
$merge_other = TRUE;
if ($other instanceof AccessResultReasonInterface) {
$result->setReason($other->getReason());
}
}
else {
if ($this instanceof AccessResultReasonInterface) {
$result->setReason($this->getReason());
}
}
}
$result->inheritCacheability($this);
......@@ -358,6 +397,14 @@ public function andIf(AccessResultInterface $other) {
$result = static::neutral();
if (!$this->isNeutral()) {
$merge_other = TRUE;
if ($other instanceof AccessResultReasonInterface) {
$result->setReason($other->getReason());
}
}
else {
if ($this instanceof AccessResultReasonInterface) {
$result->setReason($this->getReason());
}
}
}
$result->inheritCacheability($this);
......
......@@ -5,7 +5,24 @@
/**
* Value object indicating a neutral access result, with cacheability metadata.
*/
class AccessResultNeutral extends AccessResult {
class AccessResultNeutral extends AccessResult implements AccessResultReasonInterface {
/**
* The reason why access is neutral. For use in messages.
*
* @var string|null
*/
protected $reason;
/**
* Constructs a new AccessResultNeutral instance.
*
* @param null|string $reason
* (optional) a message to provide details about this access result
*/
public function __construct($reason = NULL) {
$this->reason = $reason;
}
/**
* {@inheritdoc}
......@@ -14,4 +31,19 @@ public function isNeutral() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getReason() {
return $this->reason;
}
/**
* {@inheritdoc}
*/
public function setReason($reason) {
$this->reason = $reason;
return $this;
}
}
......@@ -96,7 +96,7 @@ public function onKernelRequestFilterProvider(GetResponseEvent $event) {
if (isset($this->filter) && $event->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
$request = $event->getRequest();
if ($this->authenticationProvider->applies($request) && !$this->filter->appliesToRoutedRequest($request, TRUE)) {
throw new AccessDeniedHttpException();
throw new AccessDeniedHttpException('The used authentication method is not allowed on this route.');
}
}
}
......
......@@ -173,6 +173,12 @@ function testUnauthorizedErrorMessage() {
$this->basicAuthGet($url, $account->getUsername(), $this->randomMachineName());
$this->assertResponse('403', 'The user is blocked when wrong credentials are passed.');
$this->assertText('Access denied', "A user friendly access denied message is displayed");
// Case when correct credentials but hasn't access to the route.
$url = Url::fromRoute('router_test.15');
$this->basicAuthGet($url, $account->getUsername(), $account->pass_raw);
$this->assertResponse('403', 'The used authentication method is not allowed on this route.');
$this->assertText('Access denied', "A user friendly access denied message is displayed");
}
/**
......
......@@ -36,8 +36,13 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter
switch ($operation) {
case 'view':
return AccessResult::allowedIf($account->hasPermission('access comments') && $entity->isPublished())->cachePerPermissions()->addCacheableDependency($entity)
$access_result = AccessResult::allowedIf($account->hasPermission('access comments') && $entity->isPublished())->cachePerPermissions()->addCacheableDependency($entity)
->andIf($entity->getCommentedEntity()->access($operation, $account, TRUE));
if (!$access_result->isAllowed()) {
$access_result->setReason("The 'access comments' permission is required and the comment must be published.");
}
return $access_result;
case 'update':
return AccessResult::allowedIf($account->id() && $account->id() == $entity->getOwnerId() && $entity->isPublished() && $account->hasPermission('edit own comments'))->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity);
......
......@@ -84,10 +84,17 @@ public function testUsersWithoutPermission() {
// Ensure the text is transformed.
$this->assertRaw('<p>Do you also love Drupal?</p><figure role="group" class="caption caption-img"><img src="druplicon.png" /><figcaption>Druplicon</figcaption></figure>');
// Retrieving the untransformed text should result in an empty 403 response.
// Retrieving the untransformed text should result in an 403 response and
// return a different error message depending of the missing permission.
$response = $this->drupalPost('editor/' . 'node/1/body/en/full', '', array(), array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax')));
$this->assertResponse(403);
$this->assertIdentical('{}', $response);
if (!$user->hasPermission('access in-place editing')) {
$message = "A fatal error occurred: The 'access in-place editing' permission is required.";
$this->assertIdentical(Json::encode(['message' => $message]), $response);
}
else {
$this->assertIdentical('{}', $response);
}
}
}
......
......@@ -122,10 +122,11 @@ function testFormatPermissions() {
// Make sure that a regular user only has access to the text formats for
// which they were granted access.
$fallback_format = FilterFormat::load(filter_fallback_format());
$disallowed_format_name = $this->disallowedFormat->getPermissionName();
$this->assertTrue($this->allowedFormat->access('use', $this->webUser), 'A regular user has access to use a text format they were granted access to.');
$this->assertEqual(AccessResult::allowed()->addCacheContexts(['user.permissions']), $this->allowedFormat->access('use', $this->webUser, TRUE), 'A regular user has access to use a text format they were granted access to.');
$this->assertFalse($this->disallowedFormat->access('use', $this->webUser), 'A regular user does not have access to use a text format they were not granted access to.');
$this->assertEqual(AccessResult::neutral()->cachePerPermissions(), $this->disallowedFormat->access('use', $this->webUser, TRUE), 'A regular user does not have access to use a text format they were not granted access to.');
$this->assertEqual(AccessResult::neutral("The '$disallowed_format_name' permission is required.")->cachePerPermissions(), $this->disallowedFormat->access('use', $this->webUser, TRUE), 'A regular user does not have access to use a text format they were not granted access to.');
$this->assertTrue($fallback_format->access('use', $this->webUser), 'A regular user has access to use the fallback format.');
$this->assertEqual(AccessResult::allowed(), $fallback_format->access('use', $this->webUser, TRUE), 'A regular user has access to use the fallback format.');
......
......@@ -62,10 +62,11 @@ public function access(EntityInterface $entity, $operation, AccountInterface $ac
return $return_as_object ? $result : $result->isAllowed();
}
if (!$account->hasPermission('access content')) {
$result = AccessResult::forbidden()->cachePerPermissions();
$result = AccessResult::forbidden("The 'access content' permission is required.")->cachePerPermissions();
return $return_as_object ? $result : $result->isAllowed();
}
$result = parent::access($entity, $operation, $account, TRUE)->cachePerPermissions();
return $return_as_object ? $result : $result->isAllowed();
}
......
......@@ -112,7 +112,7 @@ public function testUserWithoutPermission() {
// Retrieving the metadata should result in an empty 403 response.
$post = array('fields[0]' => 'node/1/body/en/full');
$response = $this->drupalPostWithFormat(Url::fromRoute('quickedit.metadata'), 'json', $post);
$this->assertIdentical('{"message":""}', $response);
$this->assertIdentical(Json::encode(['message' => "The 'access in-place editing' permission is required."]), $response);
$this->assertResponse(403);
// Quick Edit's JavaScript would SearchRankingTestnever hit these endpoints if the metadata
......@@ -120,11 +120,12 @@ public function testUserWithoutPermission() {
// able to use any of the other endpoints either.
$post = array('editors[0]' => 'form') + $this->getAjaxPageStatePostData();
$response = $this->drupalPost('quickedit/attachments', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
$this->assertIdentical('{}', $response);
$message = Json::encode(['message' => "A fatal error occurred: The 'access in-place editing' permission is required."]);
$this->assertIdentical($message, $response);
$this->assertResponse(403);
$post = array('nocssjs' => 'true') + $this->getAjaxPageStatePostData();
$response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
$this->assertIdentical('{}', $response);
$this->assertIdentical($message, $response);
$this->assertResponse(403);
$edit = array();
$edit['form_id'] = 'quickedit_field_form';
......@@ -135,11 +136,11 @@ public function testUserWithoutPermission() {
$edit['body[0][format]'] = 'filtered_html';
$edit['op'] = t('Save');
$response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $edit, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
$this->assertIdentical('{}', $response);
$this->assertIdentical($message, $response);
$this->assertResponse(403);
$post = array('nocssjs' => 'true');
$response = $this->drupalPostWithFormat('quickedit/entity/' . 'node/1', 'json', $post);
$this->assertIdentical('{"message":""}', $response);
$this->assertIdentical(Json::encode(['message' => "The 'access in-place editing' permission is required."]), $response);
$this->assertResponse(403);
}
......
......@@ -120,7 +120,7 @@ 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();
throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view'));
}
$response = new ResourceResponse($entity, 200);
......@@ -160,8 +160,9 @@ 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()) {
throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'create'));
}
$definition = $this->getPluginDefinition();
// Verify that the deserialized entity is of the type that we expect to
......@@ -215,8 +216,9 @@ 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();
$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 properties.
......@@ -279,8 +281,9 @@ 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()) {
throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'delete'));
}
try {
$entity->delete();
......@@ -294,6 +297,26 @@ public function delete(EntityInterface $entity) {
}
}
/**
* Generates a fallback access denied message, when no specific reason is set.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
* @param string $operation
* The disallowed entity operation.
*
* @return string
* The proper message to display in the AccessDeniedHttpException.
*/
protected function generateFallbackAccessDeniedMessage(EntityInterface $entity, $operation) {
$message = "You are not authorized to {$operation} this {$entity->getEntityTypeId()} entity";
if ($entity->bundle() !== $entity->getEntityTypeId()) {
$message .= " of bundle {$entity->bundle()}";
}
return "{$message}.";
}
/**
* {@inheritdoc}
*/
......
services:
rest_test.authentication.test_auth:
class: Drupal\rest_test\Authentication\Provider\TestAuth
tags:
- { name: authentication_provider, provider_id: 'rest_test_auth' }
<?php
namespace Drupal\rest_test\Authentication\Provider;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Authentication provider for testing purposes.
*/
class TestAuth implements AuthenticationProviderInterface {
/**
* {@inheritdoc}
*/
public function applies(Request $request) {
return $request->headers->has('REST-test-auth');
}
/**
* {@inheritdoc}
*/
public function authenticate(Request $request) {
return NULL;
}
}
......@@ -92,7 +92,10 @@ protected function getAuthenticationRequestOptions($method) {
* {@inheritdoc}
*/
protected function assertResponseWhenMissingAuthentication(ResponseInterface $response) {
$this->assertResourceErrorResponse(403, '', $response);
// Requests needing cookie authentication but missing it results in a 403
// response. The cookie authentication mechanism sets no response message.
// @todo https://www.drupal.org/node/2847623
$this->assertResourceErrorResponse(403, FALSE, $response);
}
/**
......
......@@ -127,4 +127,20 @@ protected function getExpectedCacheTags() {
});
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
switch ($method) {
case 'GET':
return "You are not authorized to view this block entity.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
}
......@@ -310,4 +310,22 @@ public function testPostDxWithoutCriticalBaseFields() {
//$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nfield_name: This value should not be null.\n", $response);
}
/**
* {@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 'access comments' permission is required and the comment must be published.";
case 'POST';
return "The 'post comments' permission is required.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
}
......@@ -240,6 +240,43 @@ protected function getNormalizedPatchEntity() {
return $this->getNormalizedPostEntity();
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return $this->getExpectedBCUnauthorizedAccessMessage($method);
}
$permission = $this->entity->getEntityType()->getAdminPermission();
if ($permission !== FALSE) {
return "The '{$permission}' permission is required.";
}
$http_method_to_entity_operation = [
'GET' => 'view',
'POST' => 'create',
'PATCH' => 'update',
'DELETE' => 'delete',
];
$operation = $http_method_to_entity_operation[$method];
$message = sprintf('You are not authorized to %s this %s entity', $operation, $this->entity->getEntityTypeId());
if ($this->entity->bundle() !== $this->entity->getEntityTypeId()) {
$message .= ' of bundle ' . $this->entity->bundle();
}
return "$message.";
}
/**
* {@inheritdoc}
*/
protected function getExpectedBcUnauthorizedAccessMessage($method) {
return "The 'restful " . strtolower($method) . " entity:" . $this->entity->getEntityTypeId() . "' permission is required.";
}
/**
* The expected cache tags for the GET/HEAD response of the test entity.
*
......@@ -301,7 +338,7 @@ public function testGet() {
// response because ?_format query string is present.
$response = $this->request('GET', $url, $request_options);
if ($has_canonical_url) {
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response);
}
else {
$this->assertResourceErrorResponse(404, 'No route found for "GET ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '"', $response);
......@@ -335,16 +372,23 @@ public function testGet() {
$this->assertResponseWhenMissingAuthentication($response);
}
$request_options[RequestOptions::HEADERS]['REST-test-auth'] = '1';
// DX: 403 when attempting to use unallowed authentication provider.
$response = $this->request('GET', $url, $request_options);
$this->assertResourceErrorResponse(403, 'The used authentication method is not allowed on this route.', $response);
unset($request_options[RequestOptions::HEADERS]['REST-test-auth']);
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('GET'));
// DX: 403 when unauthorized.
$response = $this->request('GET', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response);
$this->assertArrayNotHasKey('Link', $response->getHeaders());
$this->setUpAuthorization('GET');
......@@ -420,8 +464,7 @@ public function testGet() {
// DX: 403 when unauthorized.
$response = $this->request('GET', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response);
$this->grantPermissionsToTestedRole(['restful get entity:' . static::$entityTypeId]);
......@@ -570,8 +613,7 @@ public function testPost() {
// DX: 403 when unauthorized.
$response = $this->request('POST', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $response);
$this->setUpAuthorization('POST');
......@@ -638,8 +680,7 @@ public function testPost() {
// DX: 403 when unauthorized.
$response = $this->request('POST', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $response);
$this->grantPermissionsToTestedRole(['restful post entity:' . static::$entityTypeId]);
......@@ -760,8 +801,7 @@ public function testPatch() {
// DX: 403 when unauthorized.
$response = $this->request('PATCH', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('PATCH'), $response);
$this->setUpAuthorization('PATCH');
......@@ -843,8 +883,7 @@ public function testPatch() {
// DX: 403 when unauthorized.
$response = $this->request('PATCH', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('PATCH'), $response);
$this->grantPermissionsToTestedRole(['restful patch entity:' . static::$entityTypeId]);
......@@ -915,8 +954,7 @@ public function testDelete() {
// DX: 403 when unauthorized.
$response = $this->request('DELETE', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('DELETE'), $response);
$this->setUpAuthorization('DELETE');
......@@ -945,8 +983,7 @@ public function testDelete() {
// DX: 403 when unauthorized.
$response = $this->request('DELETE', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('DELETE'), $response);
$this->grantPermissionsToTestedRole(['restful delete entity:' . static::$entityTypeId]);
......
......@@ -124,4 +124,22 @@ protected function getNormalizedPostEntity() {
];
}
/**
* {@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 test entity' permission is required.";
case 'POST':
return "The following permissions are required: 'administer entity_test content' OR 'administer entity_test_with_bundle content' OR 'create entity_test entity_test_with_bundle entities'.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
}
......@@ -193,4 +193,18 @@ protected function getNormalizedPostEntity() {
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
if ($method === 'GET' || $method == 'PATCH' || $method == 'DELETE') {
return "The 'access content' permission is required.";
}
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
......@@ -137,4 +137,26 @@ protected function getNormalizedPostEntity() {
];
}
/**
* {@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 'access content' permission is required.";
case 'POST':
return "The 'administer taxonomy' permission is required.";
case 'PATCH':
return "The following permissions are required: 'edit terms in camelids' OR 'administer taxonomy'.";
case 'DELETE':
return "The following permissions are required: 'delete terms in camelids' OR 'administer taxonomy'.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
}
......@@ -217,4 +217,24 @@ public function testPatchDxForSecuritySensitiveBaseFields() {
$this->assertSame(200, $response->getStatusCode());
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
switch ($method) {
case 'GET':