Commit 9b3e441c authored by xjm's avatar xjm

SA-CORE-2019-003 by samuel.mortenson, Berdir, pwolanin, dawehner,...

SA-CORE-2019-003 by samuel.mortenson, Berdir, pwolanin, dawehner, cashwilliams, Wim Leers, xjm, larowlan, alexpott, plach, damiankloip, tstoeckler, tedbow, DamienMcKenna, effulgentsia, RobLoach, gabesullice, drumm, heshanlk, dsnopek, fago, miro_dietiker, truls1502
parent 74e8c205
......@@ -16,6 +16,10 @@
* Disables any extensions that are incompatible with the current core version.
*/
function update_fix_compatibility() {
// Fix extension objects if the update is being done via Drush 8. In non-Drush
// environments this will already be fixed by the UpdateKernel this point.
UpdateKernel::fixSerializedExtensionObjects(\Drupal::getContainer());
$extension_config = \Drupal::configFactory()->getEditable('core.extension');
$save = FALSE;
foreach (['module', 'theme'] as $type) {
......@@ -30,10 +34,6 @@ function update_fix_compatibility() {
$extension_config->set('module', module_config_sort($extension_config->get('module')));
$extension_config->save();
}
// Fix extension objects if the update is being done via Drush 8. In non-Drush
// environments this will already be fixed by the UpdateKernel this point.
UpdateKernel::fixSerializedExtensionObjects(\Drupal::getContainer());
}
/**
......
......@@ -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')) {
......
......@@ -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;
}
}
}
......
......@@ -124,6 +124,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}
*/
......@@ -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;
}
......
......@@ -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);
}
}
}
......
......@@ -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);
}
}
}
......
......@@ -219,6 +219,9 @@ public static function fixSerializedExtensionObjects(ContainerInterface $contain
// will be PHP warnings. This silently fixes Drupal so that the update can
// continue.
$callable = function () use ($container) {
// Reset static caches in profile list so the module list is rebuilt
// correctly.
$container->get('extension.list.profile')->reset();
foreach ($container->getParameter('cache_bins') as $service_id => $bin) {
$container->get($service_id)->deleteAll();
}
......
......@@ -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']);
}
......
......@@ -337,8 +337,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 '';
}
}
......@@ -378,9 +380,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);
}
}
}
......@@ -66,7 +66,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);
}
......
......@@ -4,6 +4,7 @@
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
use Drupal\serialization\Normalizer\SerializedColumnNormalizerTrait;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
/**
......@@ -11,6 +12,8 @@
*/
class FieldItemNormalizer extends NormalizerBase {
use SerializedColumnNormalizerTrait;
/**
* The interface or class that this Normalizer supports.
*
......@@ -44,6 +47,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'])) {
......@@ -71,6 +75,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;
}
......
......@@ -3,6 +3,7 @@
namespace Drupal\Tests\hal\Kernel;
use Drupal\Core\Url;
use Drupal\entity_test\Entity\EntitySerializedField;
use Drupal\field\Entity\FieldConfig;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
......
......@@ -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;
/**
......
......@@ -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);
}
......
......@@ -345,7 +345,7 @@ protected function uploadFile() {
// To still run the complete test coverage for POSTing a Media entity, we
// must revoke the additional permissions that we granted.
$role = Role::load(static::$auth ? RoleInterface::AUTHENTICATED_ID : RoleInterface::AUTHENTICATED_ID);
$role = Role::load(static::$auth ? RoleInterface::AUTHENTICATED_ID : RoleInterface::ANONYMOUS_ID);
$role->revokePermission('create camelids media');
$role->trustData()->save();
}
......@@ -422,9 +422,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);
}
......
......@@ -80,7 +80,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();
}
......
......@@ -210,7 +210,7 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
if ($method === 'GET' || $method == 'PATCH' || $method == 'DELETE') {
if ($method === 'GET' || $method == 'PATCH' || $method == 'DELETE' || $method == 'POST') {
return "The 'access content' permission is required.";
}
return parent::getExpectedUnauthorizedAccessMessage($method);
......
......@@ -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.
}
......@@ -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;
......@@ -121,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 CacheableAccessDeniedHttpException($entity_access, $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) {
......@@ -223,10 +221,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 +321,6 @@ protected function checkPatchFieldAccess(FieldItemListInterface $original_field,
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public