Skip to content

Commit 8cd04b9

Browse files
authored
Merge pull request #66 from ThibBal/introspectionQueryDepthComplexity
Add new configuration properties : introspection, maximum_query_complexity and maximum_query_depth
2 parents 3c0b7d7 + 6ba4e6d commit 8cd04b9

File tree

10 files changed

+224
-8
lines changed

10 files changed

+224
-8
lines changed

DependencyInjection/Configuration.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ public function getConfigTreeBuilder()
4141
->children()
4242
->enumNode('enable_login')->values(['on', 'off', 'auto'])->defaultValue('auto')->info('Enable to automatically create a login/logout mutation. "on": enable, "auto": enable if security bundle is available.')->end()
4343
->enumNode('enable_me')->values(['on', 'off', 'auto'])->defaultValue('auto')->info('Enable to automatically create a "me" query to fetch the current user. "on": enable, "auto": enable if security bundle is available.')->end()
44+
->booleanNode('introspection')->defaultValue(true)->info('Allow the introspection of the GraphQL API.')->end()
45+
->integerNode('maximum_query_complexity')->info('Define a maximum query complexity value.')->end()
46+
->integerNode('maximum_query_depth')->info('Define a maximum query depth value.')->end()
4447
->scalarNode('firewall_name')->defaultValue('main')->info('The name of the firewall to use for login')->end()
4548
->end()
4649
->end()

DependencyInjection/GraphqliteCompilerPass.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
namespace TheCodingMachine\Graphqlite\Bundle\DependencyInjection;
55

6+
use GraphQL\Server\ServerConfig;
7+
use GraphQL\Validator\Rules\DisableIntrospection;
8+
use GraphQL\Validator\Rules\QueryComplexity;
9+
use GraphQL\Validator\Rules\QueryDepth;
610
use ReflectionNamedType;
711
use Symfony\Component\Cache\Adapter\ApcuAdapter;
812
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
@@ -163,6 +167,22 @@ public function process(ContainerBuilder $container): void
163167
}
164168
}
165169

170+
// ServerConfig rules
171+
$serverConfigDefinition = $container->findDefinition(ServerConfig::class);
172+
$rulesDefinition = [];
173+
if ($container->getParameter('graphqlite.security.introspection') === false) {
174+
$rulesDefinition[] = $container->findDefinition(DisableIntrospection::class);
175+
}
176+
if ($container->getParameter('graphqlite.security.maximum_query_complexity')) {
177+
$complexity = (int) $container->getParameter('graphqlite.security.maximum_query_complexity');
178+
$rulesDefinition[] = $container->findDefinition(QueryComplexity::class)->setArgument(0, $complexity);
179+
}
180+
if ($container->getParameter('graphqlite.security.maximum_query_depth')) {
181+
$depth = (int) $container->getParameter('graphqlite.security.maximum_query_depth');
182+
$rulesDefinition[] = $container->findDefinition(QueryDepth::class)->setArgument(0, $depth);
183+
}
184+
$serverConfigDefinition->addMethodCall('setValidationRules', [$rulesDefinition]);
185+
166186
if ($disableMe === false) {
167187
$this->registerController(MeController::class, $container);
168188
}

DependencyInjection/GraphqliteExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ public function load(array $configs, ContainerBuilder $container): void
6363
$container->setParameter('graphqlite.namespace.types', $namespaceType);
6464
$container->setParameter('graphqlite.security.enable_login', $enableLogin);
6565
$container->setParameter('graphqlite.security.enable_me', $enableMe);
66+
$container->setParameter('graphqlite.security.introspection', $configs[0]['security']['introspection'] ?? true);
67+
$container->setParameter('graphqlite.security.maximum_query_complexity', $configs[0]['security']['maximum_query_complexity'] ?? null);
68+
$container->setParameter('graphqlite.security.maximum_query_depth', $configs[0]['security']['maximum_query_depth'] ?? null);
6669
$container->setParameter('graphqlite.security.firewall_name', $configs[0]['security']['firewall_name'] ?? 'main');
6770

6871
$loader->load('graphqlite.xml');

Resources/config/container/graphqlite.xml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454

5555
<service id="TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface" alias="TheCodingMachine\Graphqlite\Bundle\Security\AuthorizationService" />
5656

57-
<service id="GraphQL\Server\ServerConfig">
57+
<service id="GraphQL\Server\ServerConfig" class="TheCodingMachine\Graphqlite\Bundle\Server\ServerConfig">
5858
<call method="setSchema">
5959
<argument type="service" id="TheCodingMachine\GraphQLite\Schema"/>
6060
</call>
@@ -72,6 +72,12 @@
7272
</call>
7373
</service>
7474

75+
<service id="GraphQL\Validator\Rules\DisableIntrospection" />
76+
77+
<service id="GraphQL\Validator\Rules\QueryComplexity" />
78+
79+
<service id="GraphQL\Validator\Rules\QueryDepth" />
80+
7581
<service id="TheCodingMachine\GraphQLite\Mappers\StaticTypeMapper">
7682
<tag name="graphql.type_mapper"/>
7783
</service>
@@ -103,7 +109,7 @@
103109
</argument>
104110
<tag name="graphql.type_mapper_factory"/>
105111
</service>
106-
112+
107113
<service id="graphqlite.phpfilescache" class="Symfony\Component\Cache\Adapter\PhpFilesAdapter">
108114
<argument>graphqlite</argument>
109115
</service>

Server/ServerConfig.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
4+
namespace TheCodingMachine\Graphqlite\Bundle\Server;
5+
6+
use GraphQL\Error\InvariantViolation;
7+
use GraphQL\Utils\Utils;
8+
use GraphQL\Validator\DocumentValidator;
9+
use GraphQL\Validator\Rules\ValidationRule;
10+
use function array_merge;
11+
use function is_array;
12+
use function is_callable;
13+
14+
/**
15+
* A slightly modified version of the server config: default validators are added by default when setValidators is called.
16+
*/
17+
class ServerConfig extends \GraphQL\Server\ServerConfig
18+
{
19+
/**
20+
* Set validation rules for this server, AND adds by default all the "default" validation rules provided by Webonyx
21+
*
22+
* @param ValidationRule[]|callable $validationRules
23+
*
24+
* @return \GraphQL\Server\ServerConfig
25+
*
26+
* @api
27+
*/
28+
public function setValidationRules($validationRules)
29+
{
30+
parent::setValidationRules(array_merge(DocumentValidator::defaultRules(), $validationRules));
31+
32+
return $this;
33+
}
34+
35+
}

Tests/Fixtures/Entities/Contact.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,12 @@ public function prefetchData(iterable $iterable, stdClass $someOtherService = nu
6666
}
6767
return 'OK';
6868
}
69+
70+
/**
71+
* @Field()
72+
*/
73+
public function getManager(): ?Contact
74+
{
75+
return null;
76+
}
6977
}

Tests/Fixtures/Entities/Product.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
namespace TheCodingMachine\Graphqlite\Bundle\Tests\Fixtures\Entities;
55

66

7+
use TheCodingMachine\GraphQLite\Annotations\Field;
8+
79
class Product
810
{
911
/**
@@ -36,6 +38,4 @@ public function getPrice(): float
3638
{
3739
return $this->price;
3840
}
39-
40-
41-
}
41+
}

Tests/Fixtures/Types/ProductType.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
namespace TheCodingMachine\Graphqlite\Bundle\Tests\Fixtures\Types;
55

6+
use TheCodingMachine\GraphQLite\Annotations\Field;
67
use TheCodingMachine\GraphQLite\Annotations\SourceField;
78
use TheCodingMachine\GraphQLite\Annotations\Type;
9+
use TheCodingMachine\Graphqlite\Bundle\Tests\Fixtures\Entities\Contact;
810
use TheCodingMachine\Graphqlite\Bundle\Tests\Fixtures\Entities\Product;
911

1012

@@ -15,5 +17,12 @@
1517
*/
1618
class ProductType
1719
{
20+
/**
21+
* @Field()
22+
*/
23+
public function getSeller(Product $product): ?Contact
24+
{
25+
return null;
26+
}
1827

19-
}
28+
}

Tests/FunctionalTest.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ public function testNoLoginNoSessionQuery(): void
312312

313313
$result = json_decode($response->getContent(), true);
314314

315+
$this->assertArrayHasKey('errors', $result);
315316
$this->assertSame('Cannot query field "login" on type "Mutation".', $result['errors'][0]['message']);
316317
}
317318

@@ -421,6 +422,110 @@ public function testValidation(): void
421422
$this->assertSame('Validate', $errors[0]['extensions']['category']);
422423
}
423424

425+
public function testWithIntrospection(): void
426+
{
427+
$kernel = new GraphqliteTestingKernel(true, null, true, null);
428+
$kernel->boot();
429+
430+
$request = Request::create('/graphql', 'POST', ['query' => '
431+
{
432+
__schema {
433+
queryType {
434+
name
435+
}
436+
}
437+
}
438+
']);
439+
440+
$response = $kernel->handle($request);
441+
442+
$result = json_decode($response->getContent(), true);
443+
$data = $result['data'];
444+
445+
$this->assertArrayHasKey('__schema', $data);
446+
}
447+
448+
public function testDisableIntrospection(): void
449+
{
450+
$kernel = new GraphqliteTestingKernel(true, null, true, null, false, 2, 2);
451+
$kernel->boot();
452+
453+
$request = Request::create('/graphql', 'POST', ['query' => '
454+
{
455+
__schema {
456+
queryType {
457+
name
458+
}
459+
}
460+
}
461+
']);
462+
463+
$response = $kernel->handle($request);
464+
465+
$result = json_decode($response->getContent(), true);
466+
$errors = $result['errors'];
467+
468+
$this->assertSame('GraphQL introspection is not allowed, but the query contained __schema or __type', $errors[0]['message']);
469+
}
470+
471+
public function testMaxQueryComplexity(): void
472+
{
473+
$kernel = new GraphqliteTestingKernel(true, null, true, null, false, 2, null);
474+
$kernel->boot();
475+
476+
$request = Request::create('/graphql', 'POST', ['query' => '
477+
{
478+
products
479+
{
480+
name,
481+
price,
482+
seller {
483+
name
484+
}
485+
}
486+
}
487+
']);
488+
489+
$response = $kernel->handle($request);
490+
491+
$result = json_decode($response->getContent(), true);
492+
$errors = $result['errors'];
493+
494+
$this->assertSame('Max query complexity should be 2 but got 5.', $errors[0]['message']);
495+
}
496+
497+
public function testMaxQueryDepth(): void
498+
{
499+
$kernel = new GraphqliteTestingKernel(true, null, true, null, false, null, 1);
500+
$kernel->boot();
501+
502+
$request = Request::create('/graphql', 'POST', ['query' => '
503+
{
504+
products
505+
{
506+
name,
507+
price,
508+
seller {
509+
name
510+
manager {
511+
name
512+
manager {
513+
name
514+
}
515+
}
516+
}
517+
}
518+
}
519+
']);
520+
521+
$response = $kernel->handle($request);
522+
523+
$result = json_decode($response->getContent(), true);
524+
$errors = $result['errors'];
525+
526+
$this->assertSame('Max query depth should be 1 but got 3.', $errors[0]['message']);
527+
}
528+
424529
private function logIn(ContainerInterface $container)
425530
{
426531
// put a token into the storage so the final calls can function

Tests/GraphqliteTestingKernel.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,29 @@ class GraphqliteTestingKernel extends Kernel
3737
* @var string|null
3838
*/
3939
private $enableMe;
40+
/**
41+
* @var bool
42+
*/
43+
private $introspection;
44+
/**
45+
* @var int|null
46+
*/
47+
private $maximumQueryComplexity;
48+
/**
49+
* @var int|null
50+
*/
51+
private $maximumQueryDepth;
4052

41-
public function __construct(bool $enableSession = true, ?string $enableLogin = null, bool $enableSecurity = true, ?string $enableMe = null)
53+
public function __construct(bool $enableSession = true, ?string $enableLogin = null, bool $enableSecurity = true, ?string $enableMe = null, bool $introspection = true, ?int $maximumQueryComplexity = null, ?int $maximumQueryDepth = null)
4254
{
4355
parent::__construct('test', true);
4456
$this->enableSession = $enableSession;
4557
$this->enableLogin = $enableLogin;
4658
$this->enableSecurity = $enableSecurity;
4759
$this->enableMe = $enableMe;
60+
$this->introspection = $introspection;
61+
$this->maximumQueryComplexity = $maximumQueryComplexity;
62+
$this->maximumQueryDepth = $maximumQueryDepth;
4863
}
4964

5065
public function registerBundles()
@@ -126,6 +141,18 @@ public function configureContainer(ContainerBuilder $c, LoaderInterface $loader)
126141
$graphqliteConf['security']['enable_me'] = $this->enableMe;
127142
}
128143

144+
if ($this->introspection === false) {
145+
$graphqliteConf['security']['introspection'] = false;
146+
}
147+
148+
if ($this->maximumQueryComplexity !== null) {
149+
$graphqliteConf['security']['maximum_query_complexity'] = $this->maximumQueryComplexity;
150+
}
151+
152+
if ($this->maximumQueryDepth !== null) {
153+
$graphqliteConf['security']['maximum_query_depth'] = $this->maximumQueryDepth;
154+
}
155+
129156
$container->loadFromExtension('graphqlite', $graphqliteConf);
130157
});
131158
$confDir = $this->getProjectDir().'/Tests/Fixtures/config';
@@ -143,6 +170,6 @@ protected function configureRoutes(RouteCollectionBuilder $routes)
143170

144171
public function getCacheDir()
145172
{
146-
return __DIR__.'/../cache/'.($this->enableSession?'withSession':'withoutSession').$this->enableLogin.($this->enableSecurity?'withSecurity':'withoutSecurity').$this->enableMe;
173+
return __DIR__.'/../cache/'.($this->enableSession?'withSession':'withoutSession').$this->enableLogin.($this->enableSecurity?'withSecurity':'withoutSecurity').$this->enableMe.'_'.($this->introspection?'withIntrospection':'withoutIntrospection').'_'.$this->maximumQueryComplexity.'_'.$this->maximumQueryDepth;
147174
}
148175
}

0 commit comments

Comments
 (0)