Skip to content

Commit

Permalink
Only expose a URL if the given user has access to the related route
Browse files Browse the repository at this point in the history
  • Loading branch information
mxr576 committed Aug 31, 2023
1 parent bf9dea3 commit 0217534
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 6 deletions.
73 changes: 68 additions & 5 deletions src/Plugin/GraphQL/DataProducer/Entity/EntityUrl.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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
* @return \Drupal\Core\Url|null
*
* @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;
}

}
39 changes: 38 additions & 1 deletion tests/src/Kernel/DataProducer/EntityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
]));
}

/**
Expand All @@ -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')
Expand Down

0 comments on commit 0217534

Please sign in to comment.