diff --git a/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php b/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php index 563355a6b7..c9266f5736 100644 --- a/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php +++ b/core/lib/Drupal/Core/Access/CsrfRequestHeaderAccessCheck.php @@ -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')) { diff --git a/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php index a6ff09430d..e0e24a2f9e 100644 --- a/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php +++ b/core/lib/Drupal/Core/Entity/EntityCreateAnyAccessCheck.php @@ -78,13 +78,15 @@ public function access(Route $route, RouteMatchInterface $route_match, AccountIn if ($entity_type->getBundleEntityType()) { $access->addCacheTags($this->entityTypeManager->getDefinition($entity_type->getBundleEntityType())->getListCacheTags()); - // Check if the user is allowed to create new bundles. If so, allow - // access, so the add page can show a link to create one. - // @see \Drupal\Core\Entity\Controller\EntityController::addPage() - $bundle_access_control_handler = $this->entityTypeManager->getAccessControlHandler($entity_type->getBundleEntityType()); - $access = $access->orIf($bundle_access_control_handler->createAccess(NULL, $account, [], TRUE)); - if ($access->isAllowed()) { - return $access; + if (empty($route->getOption('_ignore_create_bundle_access'))) { + // Check if the user is allowed to create new bundles. If so, allow + // access, so the add page can show a link to create one. + // @see \Drupal\Core\Entity\Controller\EntityController::addPage() + $bundle_access_control_handler = $this->entityTypeManager->getAccessControlHandler($entity_type->getBundleEntityType()); + $access = $access->orIf($bundle_access_control_handler->createAccess(NULL, $account, [], TRUE)); + if ($access->isAllowed()) { + return $access; + } } } diff --git a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php index be3f55e4a3..9ab36f3e31 100644 --- a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php @@ -125,6 +125,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} */ public static function getSubscribedEvents() { @@ -137,6 +152,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; } diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php index e15fe848bf..5550ede34a 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/MapItem.php @@ -64,7 +64,12 @@ public function setValue($values, $notify = TRUE) { $values = $values->getValue(); } else { - $values = unserialize($values); + if (version_compare(PHP_VERSION, '7.0.0', '>=')) { + $values = unserialize($values, ['allowed_classes' => FALSE]); + } + else { + $values = unserialize($values); + } } } diff --git a/core/lib/Drupal/Core/Routing/AccessAwareRouter.php b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php index d8487c6ef9..7607ffa29f 100644 --- a/core/lib/Drupal/Core/Routing/AccessAwareRouter.php +++ b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php @@ -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); + } } } diff --git a/core/modules/block/src/BlockAccessControlHandler.php b/core/modules/block/src/BlockAccessControlHandler.php index 35af61ed54..0ce8384788 100644 --- a/core/modules/block/src/BlockAccessControlHandler.php +++ b/core/modules/block/src/BlockAccessControlHandler.php @@ -128,7 +128,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); diff --git a/core/modules/block_content/src/BlockContentAccessControlHandler.php b/core/modules/block_content/src/BlockContentAccessControlHandler.php index d0c19c56df..e0bf064163 100644 --- a/core/modules/block_content/src/BlockContentAccessControlHandler.php +++ b/core/modules/block_content/src/BlockContentAccessControlHandler.php @@ -21,7 +21,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter if ($operation === 'view') { return AccessResult::allowed(); } - return parent::checkAccess($entity, $operation, $account); + return parent::checkAccess($entity, $operation, $account)->addCacheableDependency($entity); } } diff --git a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php index 4c4ea51f29..62678b4d2f 100644 --- a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php +++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php @@ -3,6 +3,7 @@ namespace Drupal\hal\Normalizer; use Drupal\Core\Field\FieldItemInterface; +use Drupal\serialization\Normalizer\SerializedColumnNormalizerTrait; use Symfony\Component\Serializer\Exception\InvalidArgumentException; /** @@ -10,6 +11,8 @@ */ class FieldItemNormalizer extends NormalizerBase { + use SerializedColumnNormalizerTrait; + /** * The interface or class that this Normalizer supports. * @@ -43,6 +46,7 @@ public function denormalize($data, $class, $format = NULL, array $context = []) } $field_item = $context['target_instance']; + $this->checkForSerializedStrings($data, $class, $field_item); // If this field is translatable, we need to create a translated instance. if (isset($data['lang'])) { @@ -70,6 +74,19 @@ public function denormalize($data, $class, $format = NULL, array $context = []) * The value to use in Entity::setValue(). */ protected function constructValue($data, $context) { + /** @var \Drupal\Core\Field\FieldItemInterface $field_item */ + $field_item = $context['target_instance']; + $serialized_property_names = $this->getCustomSerializedPropertyNames($field_item); + + // Explicitly serialize the input, unlike properties that rely on + // being automatically serialized, manually managed serialized properties + // expect to receive serialized input. + foreach ($serialized_property_names as $serialized_property_name) { + if (!empty($data[$serialized_property_name])) { + $data[$serialized_property_name] = serialize($data[$serialized_property_name]); + } + } + return $data; } diff --git a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php index 5d772a98ce..160ce1998c 100644 --- a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php +++ b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php @@ -191,7 +191,12 @@ public function setValue($values, $notify = TRUE) { // SqlContentEntityStorage::loadFieldItems, see // https://www.drupal.org/node/2414835 if (is_string($values['options'])) { - $values['options'] = unserialize($values['options']); + if (version_compare(PHP_VERSION, '7.0.0', '>=')) { + $values['options'] = unserialize($values['options'], ['allowed_classes' => FALSE]); + } + else { + $values['options'] = unserialize($values['options']); + } } parent::setValue($values, $notify); } diff --git a/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php b/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php index eadf04532c..b663f27071 100644 --- a/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php +++ b/core/modules/menu_link_content/src/MenuLinkContentAccessControlHandler.php @@ -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)); } } diff --git a/core/modules/node/src/NodeAccessControlHandler.php b/core/modules/node/src/NodeAccessControlHandler.php index a8a0bc6241..b0ac52088a 100644 --- a/core/modules/node/src/NodeAccessControlHandler.php +++ b/core/modules/node/src/NodeAccessControlHandler.php @@ -81,7 +81,7 @@ public function createAccess($entity_bundle = NULL, AccountInterface $account = 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(); } diff --git a/core/modules/rest/rest.post_update.php b/core/modules/rest/rest.post_update.php index 6d45178804..c15d270495 100644 --- a/core/modules/rest/rest.post_update.php +++ b/core/modules/rest/rest.post_update.php @@ -61,3 +61,10 @@ function rest_post_update_resource_granularity() { } } } + +/** + * Clear caches due to changes in route definitions. + */ +function rest_post_update_161923() { + // Empty post-update hook. +} diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index 5d9849ded4..9941923ad9 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -12,12 +12,14 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Routing\AccessAwareRouterInterface; use Drupal\Core\TypedData\PrimitiveInterface; 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,14 +122,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 AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view')); - } - + $request = \Drupal::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) { @@ -257,10 +256,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 properties. $entity_keys = $entity->getEntityType()->getKeys(); @@ -322,10 +317,6 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity * @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()]); @@ -378,6 +369,23 @@ 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 'POST': + $route->setRequirement('_entity_create_any_access', $this->entityType->id()); + $route->setOption('_ignore_create_bundle_access', TRUE); + 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') ?: []; diff --git a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php index decca43227..085e5242d0 100644 --- a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php +++ b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php @@ -11,6 +11,8 @@ */ class FieldItemNormalizer extends ComplexDataNormalizer implements DenormalizerInterface { + use SerializedColumnNormalizerTrait; + /** * {@inheritdoc} */ @@ -30,6 +32,7 @@ public function denormalize($data, $class, $format = NULL, array $context = []) /** @var \Drupal\Core\Field\FieldItemInterface $field_item */ $field_item = $context['target_instance']; + $this->checkForSerializedStrings($data, $class, $field_item); $field_item->setValue($this->constructValue($data, $context)); return $field_item; @@ -51,6 +54,19 @@ public function denormalize($data, $class, $format = NULL, array $context = []) * The value to use in Entity::setValue(). */ protected function constructValue($data, $context) { + /** @var \Drupal\Core\Field\FieldItemInterface $field_item */ + $field_item = $context['target_instance']; + $serialized_property_names = $this->getCustomSerializedPropertyNames($field_item); + + // Explicitly serialize the input, unlike properties that rely on + // being automatically serialized, manually managed serialized properties + // expect to receive serialized input. + foreach ($serialized_property_names as $serialized_property_name) { + if (!empty($data[$serialized_property_name])) { + $data[$serialized_property_name] = serialize($data[$serialized_property_name]); + } + } + return $data; } diff --git a/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php b/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php index cce108cacf..1774a11bef 100644 --- a/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php +++ b/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php @@ -2,6 +2,7 @@ namespace Drupal\serialization\Normalizer; +use Drupal\Core\Field\FieldItemInterface; use Drupal\Core\TypedData\PrimitiveInterface; /** @@ -9,6 +10,8 @@ */ class PrimitiveDataNormalizer extends NormalizerBase { + use SerializedColumnNormalizerTrait; + /** * The interface or class that this Normalizer supports. * @@ -20,6 +23,14 @@ class PrimitiveDataNormalizer extends NormalizerBase { * {@inheritdoc} */ public function normalize($object, $format = NULL, array $context = []) { + $parent = $object->getParent(); + if ($parent instanceof FieldItemInterface && $object->getValue()) { + $serialized_property_names = $this->getCustomSerializedPropertyNames($parent); + if (in_array($object->getName(), $serialized_property_names, TRUE)) { + return unserialize($object->getValue()); + } + } + // Typed data casts NULL objects to their empty variants, so for example // the empty string ('') for string type data, or 0 for integer typed data. // In a better world with typed data implementing algebraic data types, diff --git a/core/modules/serialization/src/Normalizer/SerializedColumnNormalizerTrait.php b/core/modules/serialization/src/Normalizer/SerializedColumnNormalizerTrait.php new file mode 100644 index 0000000000..bf6eb0643c --- /dev/null +++ b/core/modules/serialization/src/Normalizer/SerializedColumnNormalizerTrait.php @@ -0,0 +1,116 @@ +schema(), which requires a field + // storage definition. To determine that, the entity type and bundle + // must be known, which is contextual information that the Symfony + // serializer does not pass to ::supportsDenormalization(). + if (!is_array($data)) { + $data = [$field_item->getDataDefinition()->getMainPropertyName() => $data]; + } + if ($this->dataHasStringForSerializeColumn($field_item, $data)) { + $field_name = $field_item->getParent() ? $field_item->getParent()->getName() : $field_item->getName(); + throw new \LogicException(sprintf('The generic FieldItemNormalizer cannot denormalize string values for "%s" properties of the "%s" field (field item class: %s).', implode('", "', $this->getSerializedPropertyNames($field_item)), $field_name, $class)); + } + } + + /** + * Checks if the data contains string value for serialize column. + * + * @param \Drupal\Core\Field\FieldItemInterface $field_item + * The field item. + * @param array $data + * The data being denormalized. + * + * @return bool + * TRUE if there is a string value for serialize column, otherwise FALSE. + */ + protected function dataHasStringForSerializeColumn(FieldItemInterface $field_item, array $data) { + foreach ($this->getSerializedPropertyNames($field_item) as $property_name) { + if (isset($data[$property_name]) && is_string($data[$property_name])) { + return TRUE; + } + } + return FALSE; + } + + /** + * Gets the names of all serialized properties. + * + * @param \Drupal\Core\Field\FieldItemInterface $field_item + * The field item. + * + * @return string[] + * The property names for serialized properties. + */ + protected function getSerializedPropertyNames(FieldItemInterface $field_item) { + $field_storage_definition = $field_item->getFieldDefinition()->getFieldStorageDefinition(); + + if ($custom_property_names = $this->getCustomSerializedPropertyNames($field_item)) { + return $custom_property_names; + } + + $field_storage_schema = $field_item->schema($field_storage_definition); + // If there are no columns then there are no serialized properties. + if (!isset($field_storage_schema['columns'])) { + return []; + } + $serialized_columns = array_filter($field_storage_schema['columns'], function ($column_schema) { + return isset($column_schema['serialize']) && $column_schema['serialize'] === TRUE; + }); + return array_keys($serialized_columns); + } + + /** + * Gets the names of all properties the plugin treats as serialized data. + * + * This allows the field storage definition or entity type to provide a + * setting for serialized properties. This can be used for fields that + * handle serialized data themselves and do not rely on the serialized schema + * flag. + * + * @param \Drupal\Core\Field\FieldItemInterface $field_item + * The field item. + * + * @return string[] + * The property names for serialized properties. + */ + protected function getCustomSerializedPropertyNames(FieldItemInterface $field_item) { + if ($field_item instanceof PluginInspectionInterface) { + $definition = $field_item->getPluginDefinition(); + $serialized_fields = $field_item->getEntity()->getEntityType()->get('serialized_field_property_names'); + $field_name = $field_item->getFieldDefinition()->getName(); + if (is_array($serialized_fields) && isset($serialized_fields[$field_name]) && is_array($serialized_fields[$field_name])) { + return $serialized_fields[$field_name]; + } + if (isset($definition['serialized_property_names']) && is_array($definition['serialized_property_names'])) { + return $definition['serialized_property_names']; + } + } + return []; + } + +} diff --git a/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php b/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php index 3a55f74999..87be25d2d0 100644 --- a/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php +++ b/core/modules/shortcut/src/ShortcutSetAccessControlHandler.php @@ -20,7 +20,7 @@ class ShortcutSetAccessControlHandler extends EntityAccessControlHandler { protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { switch ($operation) { case 'view': - return AccessResult::allowedIf($account->hasPermission('access shortcuts'))->cachePerPermissions(); + return AccessResult::allowedIfHasPermission($account, 'access shortcuts'); case 'update': if ($account->hasPermission('administer shortcuts')) { return AccessResult::allowed()->cachePerPermissions(); diff --git a/core/modules/user/src/UserAccessControlHandler.php b/core/modules/user/src/UserAccessControlHandler.php index 712b32ab51..4020efb579 100644 --- a/core/modules/user/src/UserAccessControlHandler.php +++ b/core/modules/user/src/UserAccessControlHandler.php @@ -4,6 +4,7 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResultNeutral; +use Drupal\Core\Access\AccessResultReasonInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityAccessControlHandler; use Drupal\Core\Field\FieldDefinitionInterface; @@ -64,11 +65,16 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter case 'update': // Users can always edit their own account. - return AccessResult::allowedIf($account->id() == $entity->id())->cachePerUser(); + $access_result = AccessResult::allowedIf($account->id() == $entity->id())->cachePerUser(); + if (!$access_result->isAllowed() && $access_result instanceof AccessResultReasonInterface) { + $access_result->setReason("Users can only update their own account, unless they have the 'administer users' permission."); + } + return $access_result; case 'delete': // Users with 'cancel account' permission can cancel their own account. - return AccessResult::allowedIf($account->id() == $entity->id() && $account->hasPermission('cancel account'))->cachePerPermissions()->cachePerUser(); + return AccessResult::allowedIfHasPermission($account, 'cancel account') + ->andIf(AccessResult::allowedIf($account->id() == $entity->id())->cachePerUser()); } // No opinion.