From 02f29dd986f7f48144bc20e6f2ee2b22d133af3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dezs=C5=91=20BICZ=C3=93?= Date: Mon, 14 Aug 2023 11:36:30 +0200 Subject: [PATCH] Only expose a URL if the given user has access to the related route --- .../GraphQL/DataProducer/Entity/EntityUrl.php | 71 +++++++++++++++++-- tests/src/Kernel/DataProducer/EntityTest.php | 39 +++++++++- 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/src/Plugin/GraphQL/DataProducer/Entity/EntityUrl.php b/src/Plugin/GraphQL/DataProducer/Entity/EntityUrl.php index b7833b30a..e5e21a821 100644 --- a/src/Plugin/GraphQL/DataProducer/Entity/EntityUrl.php +++ b/src/Plugin/GraphQL/DataProducer/Entity/EntityUrl.php @@ -2,8 +2,14 @@ namespace Drupal\graphql\Plugin\GraphQL\DataProducer\Entity; +use Drupal\Core\Access\AccessManagerInterface; +use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\graphql\GraphQL\Execution\FieldContext; use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Returns the URL of an entity. @@ -28,11 +34,57 @@ * label = @Translation("URL Options"), * description = @Translation("Options to pass to the toUrl call"), * required = FALSE - * ) + * ), + * "access_user" = @ContextDefinition("entity:user", + * label = @Translation("User"), + * required = FALSE, + * default_value = NULL + * ), * } * ) */ -class EntityUrl extends DataProducerPluginBase { +class EntityUrl extends DataProducerPluginBase implements ContainerFactoryPluginInterface { + use DependencySerializationTrait; + + /** + * The access manager. + * + * @var \Drupal\Core\Access\AccessManagerInterface + */ + protected $accessManager; + + /** + * {@inheritdoc} + * + * @codeCoverageIgnore + */ + public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition) { + return new static( + $configuration, + $pluginId, + $pluginDefinition, + $container->get('access_manager') + ); + } + + /** + * EntityTranslation constructor. + * + * @param array $configuration + * The plugin configuration array. + * @param string $pluginId + * The plugin id. + * @param mixed $pluginDefinition + * The plugin definition. + * @param \Drupal\Core\Access\AccessManagerInterface $accessManager + * The access manager service. + * + * @codeCoverageIgnore + */ + public function __construct(array $configuration, $pluginId, $pluginDefinition, AccessManagerInterface $accessManager) { + parent::__construct($configuration, $pluginId, $pluginDefinition); + $this->accessManager = $accessManager; + } /** * Resolver. @@ -43,13 +95,24 @@ class EntityUrl extends DataProducerPluginBase { * The link relationship type, for example: canonical or edit-form. * @param array|null $options * The options to provided to the URL generator. + * @param \Drupal\Core\Session\AccountInterface|null $accessUser + * @param \Drupal\graphql\GraphQL\Execution\FieldContext $context * * @return \Drupal\Core\Url * * @throws \Drupal\Core\Entity\EntityMalformedException */ - public function resolve(EntityInterface $entity, ?string $rel, ?array $options) { - return $entity->toUrl($rel ?? 'canonical', $options ?? []); + public function resolve(EntityInterface $entity, ?string $rel, ?array $options, ?AccountInterface $accessUser, FieldContext $context) { + $url = $entity->toUrl($rel ?? 'canonical', $options ?? []); + + // @see https://www.drupal.org/project/drupal/issues/2677902 + $access = $this->accessManager->checkNamedRoute($url->getRouteName(), $url->getRouteParameters(), $accessUser, TRUE); + $context->addCacheableDependency($access); + if ($access->isAllowed()) { + return $url; + } + + return NULL; } } diff --git a/tests/src/Kernel/DataProducer/EntityTest.php b/tests/src/Kernel/DataProducer/EntityTest.php index 5d9d053ca..d674b377d 100644 --- a/tests/src/Kernel/DataProducer/EntityTest.php +++ b/tests/src/Kernel/DataProducer/EntityTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\graphql\Kernel\DataProducer; +use Drupal\Core\Access\AccessManagerInterface; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Language\LanguageInterface; use Drupal\Tests\graphql\Kernel\GraphQLTestBase; use Drupal\node\NodeInterface; @@ -273,17 +275,44 @@ public function testResolveTranslation(): void { * @covers \Drupal\graphql\Plugin\GraphQL\DataProducer\Entity\EntityUrl::resolve */ public function testResolveUrl(): void { + $accessManager = $this->getMockBuilder(AccessManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $accessManager->expects($this->exactly(2)) + ->method('checkNamedRoute') + ->willReturnCallback(static function (): AccessResult { + static $counter = 0; + switch ($counter) { + case 0: + $counter++; + return AccessResult::allowed(); + + case 1: + $counter++; + return AccessResult::forbidden(); + + default: + throw new \LogicException('The access() method should not have been called more than twice.'); + } + }); + $this->container->set('access_manager', $accessManager); + $url = $this->getMockBuilder(Url::class) ->disableOriginalConstructor() ->getMock(); + $url->method('getRouteParameters')->willReturn([]); - $this->entity->expects($this->once()) + $this->entity->expects($this->any()) ->method('toUrl') ->willReturn($url); $this->assertEquals($url, $this->executeDataProducer('entity_url', [ 'entity' => $this->entity, ])); + + $this->assertNull($this->executeDataProducer('entity_url', [ + 'entity' => $this->entity, + ])); } /** @@ -293,6 +322,14 @@ public function testResolveAbsoluteUrl(): void { $url = $this->getMockBuilder(Url::class) ->disableOriginalConstructor() ->getMock(); + $url->method('getRouteParameters')->willReturn([]); + + $accessManager = $this->getMockBuilder(AccessManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $accessManager->expects($this->once()) + ->method('checkNamedRoute')->willReturn(AccessResult::allowed()); + $this->container->set('access_manager', $accessManager); $this->entity->expects($this->once()) ->method('toUrl')