Unverified Commit 99e29476 authored by alexpott's avatar alexpott

Issue #3055889 by gabesullice, Wim Leers, Spokje, jibran, alexpott,...

Issue #3055889 by gabesullice, Wim Leers, Spokje, jibran, alexpott, tim.plunkett, larowlan: JsonApiResource\Link objects with inaccessible target urls should not be normalized
parent 3824b360
......@@ -4,6 +4,7 @@
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Routing\RouteMatchInterface;
......@@ -806,18 +807,23 @@ public function getInternalPath() {
*
* Determines whether the route is accessible or not.
*
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) Run access checks for this account. Defaults to the current
* user.
* @param \Drupal\Core\Session\AccountInterface|null $account
* (optional) Run access checks for this account. NULL for the current user.
* @param bool $return_as_object
* (optional) Defaults to FALSE.
*
* @return bool
* Returns TRUE if the user has access to the url, otherwise FALSE.
* @return bool|\Drupal\Core\Access\AccessResultInterface
* The access result. Returns a boolean if $return_as_object is FALSE (this
* is the default) and otherwise an AccessResultInterface object.
* When a boolean is returned, the result of AccessInterface::isAllowed() is
* returned, i.e. TRUE means access is explicitly allowed, FALSE means
* access is either explicitly forbidden or "no opinion".
*/
public function access(AccountInterface $account = NULL) {
public function access(AccountInterface $account = NULL, $return_as_object = FALSE) {
if ($this->isRouted()) {
return $this->accessManager()->checkNamedRoute($this->getRouteName(), $this->getRouteParameters(), $account);
return $this->accessManager()->checkNamedRoute($this->getRouteName(), $this->getRouteParameters(), $account, $return_as_object);
}
return TRUE;
return $return_as_object ? AccessResult::allowed() : TRUE;
}
/**
......
......@@ -70,6 +70,7 @@ services:
- { name: jsonapi_normalizer }
serializer.normalizer.link_collection.jsonapi:
class: Drupal\jsonapi\Normalizer\LinkCollectionNormalizer
arguments: ['@current_user']
tags:
- { name: jsonapi_normalizer }
serializer.normalizer.relationship.jsonapi:
......@@ -159,11 +160,11 @@ services:
- [setMediaRevisionAccessCheck, ['@?access_check.media.revision']] # This is only injected when the service is available.
# This is a temporary measure. JSON:API should not need to be aware of the Content Moderation module.
- [setLatestRevisionCheck, ['@?access_check.latest_revision']] # This is only injected when the service is available.
access_check.jsonapi.relationship_field_access:
class: Drupal\jsonapi\Access\RelationshipFieldAccess
access_check.jsonapi.relationship_route_access:
class: Drupal\jsonapi\Access\RelationshipRouteAccessCheck
arguments: ['@jsonapi.entity_access_checker']
tags:
- { name: access_check, applies_to: _jsonapi_relationship_field_access, needs_incoming_request: TRUE }
- { name: access_check, applies_to: _jsonapi_relationship_route_access }
# Route filters.
method_filter.jsonapi:
......@@ -246,3 +247,13 @@ services:
class: Drupal\jsonapi\Controller\TemporaryJsonapiFileFieldUploader
public: false
arguments: ['@logger.channel.file', '@file_system', '@file.mime_type.guesser', '@token', '@lock', '@config.factory']
# Deprecated services.
access_check.jsonapi.relationship_field_access:
# Deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. There is no
# replacement. JSON:API's access checkers are not part of its public API.
# See https://www.drupal.org/node/3194641.
class: Drupal\jsonapi\Access\RelationshipFieldAccess
arguments: ['@jsonapi.entity_access_checker']
tags:
- { name: access_check, applies_to: _jsonapi_relationship_field_access, needs_incoming_request: TRUE }
......@@ -2,24 +2,25 @@
namespace Drupal\jsonapi\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Session\AccountInterface;
use Drupal\jsonapi\ResourceType\ResourceType;
use Drupal\jsonapi\Routing\Routes;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Defines a class to check access to related and relationship routes.
*
* @todo Deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. There is
* no replacement. JSON:API's access checkers are not part of its public API.
*
* @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
* may change at any time and could break any dependencies on it.
*
* @see https://www.drupal.org/node/3194641
* @see https://www.drupal.org/project/drupal/issues/3032787
* @see jsonapi.api.php
*/
......@@ -63,31 +64,15 @@ public function __construct(EntityAccessChecker $entity_access_checker) {
* The access result.
*/
public function access(Request $request, Route $route, AccountInterface $account) {
$relationship_field_name = $route->getRequirement(static::ROUTE_REQUIREMENT_KEY);
$field_operation = $request->isMethodCacheable() ? 'view' : 'edit';
$entity_operation = $request->isMethodCacheable() ? 'view' : 'update';
if ($resource_type = $request->get(Routes::RESOURCE_TYPE_KEY)) {
assert($resource_type instanceof ResourceType);
$entity = $request->get('entity');
$internal_name = $resource_type->getInternalName($relationship_field_name);
if ($entity instanceof FieldableEntityInterface && $entity->hasField($internal_name)) {
$entity_access = $this->entityAccessChecker->checkEntityAccess($entity, $entity_operation, $account);
$field_access = $entity->get($internal_name)->access($field_operation, $account, TRUE);
// Ensure that access is respected for different entity revisions.
$access_result = $entity_access->andIf($field_access);
if (!$access_result->isAllowed()) {
$reason = "The current user is not allowed to {$field_operation} this relationship.";
$access_reason = $access_result instanceof AccessResultReasonInterface ? $access_result->getReason() : NULL;
$detailed_reason = empty($access_reason) ? $reason : $reason . " {$access_reason}";
$access_result->setReason($detailed_reason);
if ($request->isMethodCacheable()) {
throw new CacheableAccessDeniedHttpException(CacheableMetadata::createFromObject($access_result), $detailed_reason);
}
}
return $access_result;
}
@trigger_error(sprintf("The %s access check is deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. There is no replacement. JSON:API's route access checks are internal. See https://www.drupal.org/node/3194641.", static::ROUTE_REQUIREMENT_KEY), E_USER_DEPRECATED);
$relationship_route_access_checker = \Drupal::service('access_check.jsonapi.relationship_route_access');
assert($relationship_route_access_checker instanceof RelationshipRouteAccessCheck);
$access_result = $relationship_route_access_checker->access($route, RouteMatch::createFromRequest($request), $account);
assert($access_result instanceof AccessResultReasonInterface);
if (!$access_result->isAllowed() && $request->isMethodCacheable()) {
throw new CacheableAccessDeniedHttpException(CacheableMetadata::createFromObject($access_result), $access_result->getReason());
}
return AccessResult::neutral();
return $access_result;
}
}
<?php
namespace Drupal\jsonapi\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\jsonapi\ResourceType\ResourceType;
use Drupal\jsonapi\Routing\Routes;
use Symfony\Component\Routing\Route;
/**
* Defines a class to check access to related and relationship routes.
*
* @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
* may change at any time and could break any dependencies on it.
*
* @see https://www.drupal.org/project/drupal/issues/3032787
* @see jsonapi.api.php
*/
final class RelationshipRouteAccessCheck implements AccessInterface {
/**
* The route requirement key for this access check.
*
* @var string
*/
const ROUTE_REQUIREMENT_KEY = '_jsonapi_relationship_route_access';
/**
* The JSON:API entity access checker.
*
* @var \Drupal\jsonapi\Access\EntityAccessChecker
*/
protected $entityAccessChecker;
/**
* RelationshipRouteAccessCheck constructor.
*
* @param \Drupal\jsonapi\Access\EntityAccessChecker $entity_access_checker
* The JSON:API entity access checker.
*/
public function __construct(EntityAccessChecker $entity_access_checker) {
$this->entityAccessChecker = $entity_access_checker;
}
/**
* Checks access to the relationship field on the given route.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account = NULL) {
[$relationship_field_name, $field_operation] = explode('.', $route->getRequirement(static::ROUTE_REQUIREMENT_KEY));
assert(in_array($field_operation, ['view', 'edit'], TRUE));
$entity_operation = $field_operation === 'view' ? 'view' : 'update';
if ($resource_type = $route_match->getParameter(Routes::RESOURCE_TYPE_KEY)) {
assert($resource_type instanceof ResourceType);
$entity = $route_match->getParameter('entity');
$internal_name = $resource_type->getInternalName($relationship_field_name);
if ($entity instanceof FieldableEntityInterface && $entity->hasField($internal_name)) {
$entity_access = $this->entityAccessChecker->checkEntityAccess($entity, $entity_operation, $account);
$field_access = $entity->get($internal_name)->access($field_operation, $account, TRUE);
// Ensure that access is respected for different entity revisions.
$access_result = $entity_access->andIf($field_access);
if (!$access_result->isAllowed()) {
$reason = "The current user is not allowed to {$field_operation} this relationship.";
$access_reason = $access_result instanceof AccessResultReasonInterface ? $access_result->getReason() : NULL;
$detailed_reason = empty($access_reason) ? $reason : $reason . " {$access_reason}";
$access_result->setReason($detailed_reason);
}
return $access_result;
}
}
return AccessResult::neutral();
}
}
......@@ -564,7 +564,7 @@ function (EntityInterface $entity) {
public function getRelationship(ResourceType $resource_type, FieldableEntityInterface $entity, $related, Request $request, $response_code = 200) {
/* @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $field_list */
$field_list = $entity->get($resource_type->getInternalName($related));
// Access will have already been checked by the RelationshipFieldAccess
// Access will have already been checked by the RelationshipRouteAccessCheck
// service, so we don't need to call ::getAccessCheckedResourceObject().
$resource_object = ResourceObject::createFromEntity($resource_type, $entity);
$relationship = Relationship::createFromEntityReferenceField($resource_object, $field_list);
......
......@@ -3,9 +3,12 @@
namespace Drupal\jsonapi\Normalizer;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Session\AccountInterface;
use Drupal\jsonapi\JsonApiResource\LinkCollection;
use Drupal\jsonapi\JsonApiResource\Link;
use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
use Drupal\jsonapi\Normalizer\Value\CacheableOmission;
/**
* Normalizes a LinkCollection object.
......@@ -63,6 +66,27 @@ class LinkCollectionNormalizer extends NormalizerBase {
*/
protected $hashSalt;
/**
* The current user making the request.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* LinkCollectionNormalizer constructor.
*
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(AccountInterface $current_user = NULL) {
if (is_null($current_user)) {
@trigger_error('Calling ' . __METHOD__ . '() without the $current_user argument is deprecated in drupal:9.2.0 and will be required in drupal:10.0.0.', E_USER_DEPRECATED);
$current_user = \Drupal::currentUser();
}
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
......@@ -76,7 +100,18 @@ public function normalize($object, $format = NULL, array $context = []) {
$link_key = $is_multiple ? sprintf('%s--%s', $key, $this->hashByHref($link)) : $key;
$attributes = $link->getTargetAttributes();
$normalization = array_merge(['href' => $link->getHref()], !empty($attributes) ? ['meta' => $attributes] : []);
$normalized[$link_key] = new CacheableNormalization($link, $normalization);
// Checking access on links is not about access to the link itself;
// it is about whether the current user has access to the route that is
// *targeted* by the link. This is done on a "best effort" basis. That
// is, some links target routes that depend on a request to determine if
// they're accessible or not. Some other links might target routes to
// which the current user will clearly not have access, in that case
// this code proactively removes those links from the response.
$access = $link->getUri()->access($this->currentUser, TRUE);
$cacheability = CacheableMetadata::createFromObject($link)->addCacheableDependency($access);
$normalized[$link_key] = $access->isAllowed()
? new CacheableNormalization($cacheability, $normalization)
: new CacheableOmission($cacheability);
}
}
return CacheableNormalization::aggregate($normalized);
......
......@@ -3,7 +3,7 @@
namespace Drupal\jsonapi\Routing;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\jsonapi\Access\RelationshipFieldAccess;
use Drupal\jsonapi\Access\RelationshipRouteAccessCheck;
use Drupal\jsonapi\Controller\EntryPoint;
use Drupal\jsonapi\ParamConverter\ResourceTypeConverter;
use Drupal\jsonapi\ResourceType\ResourceType;
......@@ -316,7 +316,6 @@ protected static function getIndividualRoutesForResourceType(ResourceType $resou
$relationship_route = new Route("/{$path}/{entity}/relationships/{$relationship_field_name}");
$relationship_route->addDefaults(['_on_relationship' => TRUE]);
$relationship_route->addDefaults(['related' => $relationship_field_name]);
$relationship_route->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name);
$relationship_route->setRequirement('_csrf_request_header_token', 'TRUE');
$relationship_route_methods = $resource_type->isMutable()
? ['GET', 'POST', 'PATCH', 'DELETE']
......@@ -329,6 +328,8 @@ protected static function getIndividualRoutesForResourceType(ResourceType $resou
];
foreach ($relationship_route_methods as $method) {
$method_specific_relationship_route = clone $relationship_route;
$field_operation = $method === 'GET' ? 'view' : 'edit';
$method_specific_relationship_route->setRequirement(RelationshipRouteAccessCheck::ROUTE_REQUIREMENT_KEY, "$relationship_field_name.$field_operation");
$method_specific_relationship_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ":{$relationship_controller_methods[$method]}"]);
$method_specific_relationship_route->setMethods($method);
$routes->add(static::getRouteName($resource_type, sprintf("%s.relationship.%s", $relationship_field_name, strtolower($method))), $method_specific_relationship_route);
......@@ -342,7 +343,7 @@ protected static function getIndividualRoutesForResourceType(ResourceType $resou
$related_route->setMethods(['GET']);
$related_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':getRelated']);
$related_route->addDefaults(['related' => $relationship_field_name]);
$related_route->setRequirement(RelationshipFieldAccess::ROUTE_REQUIREMENT_KEY, $relationship_field_name);
$related_route->setRequirement(RelationshipRouteAccessCheck::ROUTE_REQUIREMENT_KEY, "$relationship_field_name.view");
$routes->add(static::getRouteName($resource_type, "$relationship_field_name.related"), $related_route);
}
}
......
......@@ -23,6 +23,7 @@ class ResourceObjectNormalizerCacherTest extends KernelTestBase {
* {@inheritdoc}
*/
protected static $modules = [
'system',
'serialization',
'jsonapi',
'user',
......@@ -54,6 +55,11 @@ class ResourceObjectNormalizerCacherTest extends KernelTestBase {
*/
protected function setUp(): void {
parent::setUp();
// Add the entity schemas.
$this->installEntitySchema('user');
// Add the additional table schemas.
$this->installSchema('system', ['sequences']);
$this->installSchema('user', ['users_data']);
$this->resourceTypeRepository = $this->container->get('jsonapi.resource_type.repository');
$this->serializer = $this->container->get('jsonapi.serializer');
$this->cacher = $this->container->get('jsonapi.normalization_cacher');
......@@ -69,6 +75,7 @@ public function testLinkNormalizationCacheability() {
'name' => $this->randomMachineName(),
'pass' => $this->randomString(),
]);
$user->save();
$resource_type = $this->resourceTypeRepository->get($user->getEntityTypeId(), $user->bundle());
$resource_object = ResourceObject::createFromEntity($resource_type, $user);
$cache_tag_to_invalidate = 'link_normalization';
......
......@@ -3,13 +3,16 @@
namespace Drupal\Tests\jsonapi\Kernel\Normalizer;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\jsonapi\JsonApiResource\Link;
use Drupal\jsonapi\JsonApiResource\LinkCollection;
use Drupal\jsonapi\JsonApiResource\ResourceObject;
use Drupal\jsonapi\Normalizer\LinkCollectionNormalizer;
use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
use Drupal\jsonapi\ResourceType\ResourceType;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* @coversDefaultClass \Drupal\jsonapi\Normalizer\LinkCollectionNormalizer
......@@ -19,19 +22,37 @@
*/
class LinkCollectionNormalizerTest extends KernelTestBase {
use UserCreationTrait;
/**
* The serializer.
*
* @var \Symfony\Component\Serializer\SerializerInterface
*/
protected $serializer;
/**
* The subject under test.
*
* @var \Symfony\Component\Serializer\Normalizer\NormalizerInterface
* @var \Drupal\jsonapi\Normalizer\LinkCollectionNormalizer
*/
protected $normalizer;
/**
* Test users.
*
* @var \Drupal\user\UserInterface[]
*/
protected $testUsers;
/**
* {@inheritDoc}
*/
protected static $modules = [
'jsonapi',
'serialization',
'system',
'user',
];
/**
......@@ -39,8 +60,16 @@ class LinkCollectionNormalizerTest extends KernelTestBase {
*/
protected function setUp(): void {
parent::setUp();
$this->normalizer = new LinkCollectionNormalizer();
$this->normalizer->setSerializer($this->container->get('jsonapi.serializer'));
// Add the entity schemas.
$this->installEntitySchema('user');
// Add the additional table schemas.
$this->installSchema('system', ['sequences']);
$this->installSchema('user', ['users_data']);
// Set the user IDs to something higher than 1 so these users cannot be
// mistaken for the site admin.
$this->testUsers[] = $this->createUser([], NULL, FALSE, ['uid' => 2]);
$this->testUsers[] = $this->createUser([], NULL, FALSE, ['uid' => 3]);
$this->serializer = $this->container->get('jsonapi.serializer');
}
/**
......@@ -52,7 +81,8 @@ public function testNormalize() {
->withLink('related', new Link(new CacheableMetadata(), Url::fromUri('http://example.com/post/42'), 'related', ['title' => 'Most viewed']))
->withLink('related', new Link(new CacheableMetadata(), Url::fromUri('http://example.com/post/42'), 'related', ['title' => 'Top rated']))
->withContext($link_context);
$normalized = $this->normalizer->normalize($link_collection)->getNormalization();
// Create the SUT.
$normalized = $this->getNormalizer()->normalize($link_collection)->getNormalization();
$this->assertIsArray($normalized);
foreach (array_keys($normalized) as $key) {
$this->assertStringStartsWith('related', $key);
......@@ -73,4 +103,101 @@ public function testNormalize() {
], array_values($normalized));
}
/**
* Tests the link collection normalizer.
*
* @dataProvider linkAccessTestData
*/
public function testLinkAccess($current_user_id, $edit_form_uid, $expected_link_keys, $expected_cache_contexts) {
// Get the current user and an edit-form URL.
foreach ($this->testUsers as $user) {
$uid = (int) $user->id();
if ($uid === $current_user_id) {
$current_user = $user;
}
if ($uid === $edit_form_uid) {
$edit_form_url = $user->toUrl('edit-form');
}
}
assert(isset($current_user));
assert(isset($edit_form_url));
// Create a link collection to normalize.
$mock_resource_object = $this->createMock(ResourceObject::class);
$link_collection = new LinkCollection([
'edit-form' => new Link(new CacheableMetadata(), $edit_form_url, 'edit-form', ['title' => 'Edit']),
]);
$link_collection = $link_collection->withContext($mock_resource_object);
// Normalize the collection.
$actual_normalization = $this->getNormalizer($current_user)->normalize($link_collection);
// Check that it returned the expected value object.
$this->assertInstanceOf(CacheableNormalization::class, $actual_normalization);
// Get the raw normalized data.
$actual_data = $actual_normalization->getNormalization();
$this->assertIsArray($actual_data);
// Check that the expected links are present and unexpected links are
// absent.
$actual_link_keys = array_keys($actual_data);
sort($expected_link_keys);
sort($actual_link_keys);
$this->assertSame($expected_link_keys, $actual_link_keys);
// Check that the expected cache contexts were added.
$actual_cache_contexts = $actual_normalization->getCacheContexts();
sort($expected_cache_contexts);
sort($actual_cache_contexts);
$this->assertSame($expected_cache_contexts, $actual_cache_contexts);
// If the edit-form link was present, check that it has the correct href.
if (isset($actual_data['edit-form'])) {
$this->assertSame($actual_data['edit-form'], [
'href' => $edit_form_url->setAbsolute()->toString(),
'meta' => [
'title' => 'Edit',
],
]);
}
}
/**
* Provides test cases for testing link access checking.
*
* @return array[]
*/
public function linkAccessTestData() {
return [
'the edit-form link is present because uid 2 has access to the targeted resource (its own edit form)' => [
'uid' => 2,
'edit-form uid' => 2,
'expected link keys' => ['edit-form'],
'expected cache contexts' => ['url.site', 'user'],
],
"the edit-form link is omitted because uid 3 doesn't have access to the targeted resource (another account's edit form)" => [
'uid' => 3,
'edit-form uid' => 2,
'expected link keys' => [],
'expected cache contexts' => ['url.site', 'user'],
],
];
}
/**
* Get an instance of the normalizer to test.
*/
protected function getNormalizer(AccountInterface $current_user = NULL) {
if (is_null($current_user)) {
$current_user = $this->setUpCurrentUser();
}
else {
$this->setCurrentUser($current_user);
}
$normalizer = new LinkCollectionNormalizer($current_user);
$normalizer->setSerializer($this->serializer);
return $normalizer;
}
}
......@@ -3,7 +3,6 @@
namespace Drupal\Tests\jsonapi\Kernel\Normalizer;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
......@@ -15,6 +14,7 @@
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\user\Entity\User;
/**
......@@ -25,6 +25,8 @@
*/
class RelationshipNormalizerTest extends JsonapiKernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
......@@ -101,7 +103,7 @@ protected function setUp(): void {
FieldConfig::create(['field_name' => 'field_images', 'label' => 'Images'] + $field_config)->save();
// Set up the test data.
$this->account = $this->prophesize(AccountInterface::class)->reveal();
$this->setUpCurrentUser([], ['access content']);
$this->user1 = User::create([
'name' => $this->randomMachineName(),
'mail' => $this->randomMachineName() . '@example.com',
......