Add Complexity and Depth Query Security

This commit is contained in:
Jeremiah VALERIE 2016-04-09 10:04:14 +02:00
parent 1bc5e0c9da
commit 545fe616a0
11 changed files with 1030 additions and 6 deletions

View File

@ -462,6 +462,42 @@ header('Content-Type: application/json');
echo json_encode($result);
```
### Security
#### Query Complexity Analysis
This is a PHP port of [Query Complexity Analysis](http://sangria-graphql.org/learn/#query-complexity-analysis) in Sangria implementation.
Introspection query with description max complexity is **109**.
This document validator rule is disabled by default. Here an example to enabled it:
```php
use GraphQL\GraphQL;
/** @var \GraphQL\Validator\Rules\QueryComplexity $queryComplexity */
$queryComplexity = DocumentValidator::getRule('QueryComplexity');
$queryComplexity->setMaxQueryComplexity($maxQueryComplexity = 110);
GraphQL::execute(/*...*/);
```
#### Limiting Query Depth
This is a PHP port of [Limiting Query Depth](http://sangria-graphql.org/learn/#limiting-query-depth) in Sangria implementation.
Introspection query with description max depth is **7**.
This document validator rule is disabled by default. Here an example to enabled it:
```php
use GraphQL\GraphQL;
/** @var \GraphQL\Validator\Rules\QueryDepth $queryDepth */
$queryDepth = DocumentValidator::getRule('QueryDepth');
$queryDepth->setMaxQueryDepth($maxQueryDepth = 10);
GraphQL::execute(/*...*/);
```
### More Examples
Make sure to check [tests](https://github.com/webonyx/graphql-php/tree/master/tests) for more usage examples.

View File

@ -6,6 +6,7 @@ use GraphQL\Executor\Executor;
use GraphQL\Language\Parser;
use GraphQL\Language\Source;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\QueryComplexity;
class GraphQL
{
@ -35,6 +36,11 @@ class GraphQL
try {
$source = new Source($requestString ?: '', 'GraphQL request');
$documentAST = Parser::parse($source);
/** @var QueryComplexity $queryComplexity */
$queryComplexity = DocumentValidator::getRule('QueryComplexity');
$queryComplexity->setRawVariableValues($variableValues);
$validationErrors = DocumentValidator::validate($schema, $documentAST);
if (!empty($validationErrors)) {

View File

@ -5,6 +5,8 @@ use GraphQL\Utils;
class FieldDefinition
{
const DEFAULT_COMPLEXITY_FN = 'GraphQL\Type\Definition\FieldDefinition::defaultComplexity';
/**
* @var string
*/
@ -72,6 +74,7 @@ class FieldDefinition
'map' => Config::CALLBACK,
'description' => Config::STRING,
'deprecationReason' => Config::STRING,
'complexity' => Config::CALLBACK,
]);
}
@ -113,6 +116,8 @@ class FieldDefinition
$this->deprecationReason = isset($config['deprecationReason']) ? $config['deprecationReason'] : null;
$this->config = $config;
$this->complexityFn = isset($config['complexity']) ? $config['complexity'] : static::DEFAULT_COMPLEXITY_FN;
}
/**
@ -141,4 +146,17 @@ class FieldDefinition
}
return $this->resolvedType;
}
/**
* @return callable|\Closure
*/
public function getComplexityFn()
{
return $this->complexityFn;
}
public static function defaultComplexity($childrenComplexity)
{
return $childrenComplexity + 1;
}
}

View File

@ -34,6 +34,8 @@ use GraphQL\Validator\Rules\NoUnusedVariables;
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
use GraphQL\Validator\Rules\PossibleFragmentSpreads;
use GraphQL\Validator\Rules\ProvidedNonNullArguments;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\Rules\ScalarLeafs;
use GraphQL\Validator\Rules\VariablesAreInputTypes;
use GraphQL\Validator\Rules\VariablesInAllowedPosition;
@ -82,6 +84,9 @@ class DocumentValidator
'DefaultValuesOfCorrectType' => new DefaultValuesOfCorrectType(),
'VariablesInAllowedPosition' => new VariablesInAllowedPosition(),
'OverlappingFieldsCanBeMerged' => new OverlappingFieldsCanBeMerged(),
// Query Security
'QueryDepth' => new QueryDepth(QueryDepth::DISABLED), // default disabled
'QueryComplexity' => new QueryComplexity(QueryComplexity::DISABLED), // default disabled
];
}
@ -90,7 +95,9 @@ class DocumentValidator
public static function getRule($name)
{
return isset(self::$rules[$name]) ? self::$rules[$name] : null ;
$rules = static::allRules();
return isset($rules[$name]) ? $rules[$name] : null ;
}
public static function addRule($name, callable $rule)
@ -98,11 +105,6 @@ class DocumentValidator
self::$rules[$name] = $rule;
}
public static function removeRule($name)
{
unset(self::$rules[$name]);
}
public static function validate(Schema $schema, Document $ast, array $rules = null)
{
$errors = static::visitUsingRules($schema, $ast, $rules ?: static::allRules());

View File

@ -0,0 +1,171 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\FragmentDefinition;
use GraphQL\Language\AST\FragmentSpread;
use GraphQL\Language\AST\InlineFragment;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\SelectionSet;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Introspection;
use GraphQL\Utils\TypeInfo;
use GraphQL\Validator\ValidationContext;
abstract class AbstractQuerySecurity
{
const DISABLED = 0;
/** @var FragmentDefinition[] */
private $fragments = [];
/**
* @return \GraphQL\Language\AST\FragmentDefinition[]
*/
protected function getFragments()
{
return $this->fragments;
}
/**
* check if equal to 0 no check is done. Must be greater or equal to 0.
*
* @param $value
*/
protected function checkIfGreaterOrEqualToZero($name, $value)
{
if ($value < 0) {
throw new \InvalidArgumentException(sprintf('$%s argument must be greater or equal to 0.', $name));
}
}
protected function gatherFragmentDefinition(ValidationContext $context)
{
// Gather all the fragment definition.
// Importantly this does not include inline fragments.
$definitions = $context->getDocument()->definitions;
foreach ($definitions as $node) {
if ($node instanceof FragmentDefinition) {
$this->fragments[$node->name->value] = $node;
}
}
}
protected function getFragment(FragmentSpread $fragmentSpread)
{
$spreadName = $fragmentSpread->name->value;
$fragments = $this->getFragments();
return isset($fragments[$spreadName]) ? $fragments[$spreadName] : null;
}
protected function invokeIfNeeded(ValidationContext $context, array $validators)
{
// is disabled?
if (!$this->isEnabled()) {
return [];
}
$this->gatherFragmentDefinition($context);
return $validators;
}
/**
* Given a selectionSet, adds all of the fields in that selection to
* the passed in map of fields, and returns it at the end.
*
* Note: This is not the same as execution's collectFields because at static
* time we do not know what object type will be used, so we unconditionally
* spread in all fragments.
*
* @see GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged
*
* @param ValidationContext $context
* @param Type|null $parentType
* @param SelectionSet $selectionSet
* @param \ArrayObject $visitedFragmentNames
* @param \ArrayObject $astAndDefs
*
* @return \ArrayObject
*/
protected function collectFieldASTsAndDefs(ValidationContext $context, $parentType, SelectionSet $selectionSet, \ArrayObject $visitedFragmentNames = null, \ArrayObject $astAndDefs = null)
{
$_visitedFragmentNames = $visitedFragmentNames ?: new \ArrayObject();
$_astAndDefs = $astAndDefs ?: new \ArrayObject();
foreach ($selectionSet->selections as $selection) {
switch ($selection->kind) {
case Node::FIELD:
/* @var Field $selection */
$fieldName = $selection->name->value;
$fieldDef = null;
if ($parentType && method_exists($parentType, 'getFields')) {
$tmp = $parentType->getFields();
$schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
$typeMetaFieldDef = Introspection::typeMetaFieldDef();
$typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
if ($fieldName === $schemaMetaFieldDef->name && $context->getSchema()->getQueryType() === $parentType) {
$fieldDef = $schemaMetaFieldDef;
} elseif ($fieldName === $typeMetaFieldDef->name && $context->getSchema()->getQueryType() === $parentType) {
$fieldDef = $typeMetaFieldDef;
} elseif ($fieldName === $typeNameMetaFieldDef->name) {
$fieldDef = $typeNameMetaFieldDef;
} elseif (isset($tmp[$fieldName])) {
$fieldDef = $tmp[$fieldName];
}
}
$responseName = $this->getFieldName($selection);
if (!isset($_astAndDefs[$responseName])) {
$_astAndDefs[$responseName] = new \ArrayObject();
}
// create field context
$_astAndDefs[$responseName][] = [$selection, $fieldDef];
break;
case Node::INLINE_FRAGMENT:
/* @var InlineFragment $selection */
$_astAndDefs = $this->collectFieldASTsAndDefs(
$context,
TypeInfo::typeFromAST($context->getSchema(), $selection->typeCondition),
$selection->selectionSet,
$_visitedFragmentNames,
$_astAndDefs
);
break;
case Node::FRAGMENT_SPREAD:
/* @var FragmentSpread $selection */
$fragName = $selection->name->value;
if (empty($_visitedFragmentNames[$fragName])) {
$_visitedFragmentNames[$fragName] = true;
$fragment = $context->getFragment($fragName);
if ($fragment) {
$_astAndDefs = $this->collectFieldASTsAndDefs(
$context,
TypeInfo::typeFromAST($context->getSchema(), $fragment->typeCondition),
$fragment->selectionSet,
$_visitedFragmentNames,
$_astAndDefs
);
}
}
break;
}
}
return $_astAndDefs;
}
protected function getFieldName(Field $node)
{
$fieldName = $node->name->value;
$responseName = $node->alias ? $node->alias->value : $fieldName;
return $responseName;
}
abstract protected function isEnabled();
}

View File

@ -0,0 +1,212 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error;
use GraphQL\Executor\Values;
use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\FragmentSpread;
use GraphQL\Language\AST\InlineFragment;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Language\AST\SelectionSet;
use GraphQL\Language\Visitor;
use GraphQL\Type\Definition\FieldDefinition;
use GraphQL\Validator\ValidationContext;
class QueryComplexity extends AbstractQuerySecurity
{
private $maxQueryComplexity;
private $rawVariableValues = [];
private $variableDefs;
private $fieldAstAndDefs;
/**
* @var ValidationContext
*/
private $context;
public function __construct($maxQueryDepth)
{
$this->setMaxQueryComplexity($maxQueryDepth);
}
public static function maxQueryComplexityErrorMessage($max, $count)
{
return sprintf('Max query complexity should be %d but got %d.', $max, $count);
}
/**
* Set max query complexity. If equal to 0 no check is done. Must be greater or equal to 0.
*
* @param $maxQueryComplexity
*/
public function setMaxQueryComplexity($maxQueryComplexity)
{
$this->checkIfGreaterOrEqualToZero('maxQueryComplexity', $maxQueryComplexity);
$this->maxQueryComplexity = (int) $maxQueryComplexity;
}
public function getMaxQueryComplexity()
{
return $this->maxQueryComplexity;
}
public function setRawVariableValues(array $rawVariableValues = null)
{
$this->rawVariableValues = $rawVariableValues ?: [];
}
public function getRawVariableValues()
{
return $this->rawVariableValues;
}
public function __invoke(ValidationContext $context)
{
$this->context = $context;
$this->variableDefs = new \ArrayObject();
$this->fieldAstAndDefs = new \ArrayObject();
$complexity = 0;
return $this->invokeIfNeeded(
$context,
[
// Visit FragmentDefinition after visiting FragmentSpread
'visitSpreadFragments' => true,
Node::SELECTION_SET => function (SelectionSet $selectionSet) use ($context) {
$this->fieldAstAndDefs = $this->collectFieldASTsAndDefs(
$context,
$context->getParentType(),
$selectionSet,
null,
$this->fieldAstAndDefs
);
},
Node::VARIABLE_DEFINITION => function ($def) {
$this->variableDefs[] = $def;
return Visitor::skipNode();
},
Node::OPERATION_DEFINITION => [
'leave' => function (OperationDefinition $operationDefinition) use ($context, &$complexity) {
$complexity = $this->fieldComplexity($operationDefinition, $complexity);
if ($complexity > $this->getMaxQueryComplexity()) {
return new Error($this->maxQueryComplexityErrorMessage($this->getMaxQueryComplexity(), $complexity));
}
},
],
]
);
}
private function fieldComplexity($node, $complexity = 0)
{
if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSet) {
foreach ($node->selectionSet->selections as $childNode) {
$complexity = $this->nodeComplexity($childNode, $complexity);
}
}
return $complexity;
}
private function nodeComplexity(Node $node, $complexity = 0)
{
switch ($node->kind) {
case Node::FIELD:
/* @var Field $node */
// default values
$args = [];
$complexityFn = FieldDefinition::DEFAULT_COMPLEXITY_FN;
// calculate children complexity if needed
$childrenComplexity = 0;
// node has children?
if (isset($node->selectionSet)) {
$childrenComplexity = $this->fieldComplexity($node);
}
$astFieldInfo = $this->astFieldInfo($node);
$fieldDef = $astFieldInfo[1];
if ($fieldDef instanceof FieldDefinition) {
$args = $this->buildFieldArguments($node);
//get complexity fn using fieldDef complexity
if (method_exists($fieldDef, 'getComplexityFn')) {
$complexityFn = $fieldDef->getComplexityFn();
}
}
$complexity += call_user_func_array($complexityFn, [$childrenComplexity, $args]);
break;
case Node::INLINE_FRAGMENT:
/* @var InlineFragment $node */
// node has children?
if (isset($node->selectionSet)) {
$complexity = $this->fieldComplexity($node, $complexity);
}
break;
case Node::FRAGMENT_SPREAD:
/* @var FragmentSpread $node */
$fragment = $this->getFragment($node);
if (null !== $fragment) {
$complexity = $this->fieldComplexity($fragment, $complexity);
}
break;
}
return $complexity;
}
private function astFieldInfo(Field $field)
{
$fieldName = $this->getFieldName($field);
$astFieldInfo = [null, null];
if (isset($this->fieldAstAndDefs[$fieldName])) {
foreach ($this->fieldAstAndDefs[$fieldName] as $astAndDef) {
if ($astAndDef[0] == $field) {
$astFieldInfo = $astAndDef;
break;
}
}
}
return $astFieldInfo;
}
private function buildFieldArguments(Field $node)
{
$rawVariableValues = $this->getRawVariableValues();
$astFieldInfo = $this->astFieldInfo($node);
$fieldDef = $astFieldInfo[1];
$args = [];
if ($fieldDef instanceof FieldDefinition) {
$variableValues = Values::getVariableValues(
$this->context->getSchema(),
$this->variableDefs,
$rawVariableValues
);
$args = Values::getArgumentValues($fieldDef->args, $node->arguments, $variableValues);
}
return $args;
}
protected function isEnabled()
{
return $this->getMaxQueryComplexity() !== static::DISABLED;
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace GraphQL\Validator\Rules;
use GraphQL\Error;
use GraphQL\Language\AST\Field;
use GraphQL\Language\AST\FragmentSpread;
use GraphQL\Language\AST\InlineFragment;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Language\AST\SelectionSet;
use GraphQL\Validator\ValidationContext;
class QueryDepth extends AbstractQuerySecurity
{
/**
* @var int
*/
private $maxQueryDepth;
public function __construct($maxQueryDepth)
{
$this->setMaxQueryDepth($maxQueryDepth);
}
/**
* Set max query depth. If equal to 0 no check is done. Must be greater or equal to 0.
*
* @param $maxQueryDepth
*/
public function setMaxQueryDepth($maxQueryDepth)
{
$this->checkIfGreaterOrEqualToZero('maxQueryDepth', $maxQueryDepth);
$this->maxQueryDepth = (int) $maxQueryDepth;
}
public function getMaxQueryDepth()
{
return $this->maxQueryDepth;
}
public static function maxQueryDepthErrorMessage($max, $count)
{
return sprintf('Max query depth should be %d but got %d.', $max, $count);
}
public function __invoke(ValidationContext $context)
{
return $this->invokeIfNeeded(
$context,
[
Node::OPERATION_DEFINITION => [
'leave' => function (OperationDefinition $operationDefinition) use ($context) {
$maxDepth = $this->fieldDepth($operationDefinition);
if ($maxDepth > $this->getMaxQueryDepth()) {
return new Error($this->maxQueryDepthErrorMessage($this->getMaxQueryDepth(), $maxDepth));
}
},
],
]
);
}
protected function isEnabled()
{
return $this->getMaxQueryDepth() !== static::DISABLED;
}
private function fieldDepth($node, $depth = 0, $maxDepth = 0)
{
if (isset($node->selectionSet) && $node->selectionSet instanceof SelectionSet) {
foreach ($node->selectionSet->selections as $childNode) {
$maxDepth = $this->nodeDepth($childNode, $depth, $maxDepth);
}
}
return $maxDepth;
}
private function nodeDepth(Node $node, $depth = 0, $maxDepth = 0)
{
switch ($node->kind) {
case Node::FIELD:
/* @var Field $node */
// node has children?
if (null !== $node->selectionSet) {
// update maxDepth if needed
if ($depth > $maxDepth) {
$maxDepth = $depth;
}
$maxDepth = $this->fieldDepth($node, $depth + 1, $maxDepth);
}
break;
case Node::INLINE_FRAGMENT:
/* @var InlineFragment $node */
// node has children?
if (null !== $node->selectionSet) {
$maxDepth = $this->fieldDepth($node, $depth, $maxDepth);
}
break;
case Node::FRAGMENT_SPREAD:
/* @var FragmentSpread $node */
$fragment = $this->getFragment($node);
if (null !== $fragment) {
$maxDepth = $this->fieldDepth($fragment, $depth, $maxDepth);
}
break;
}
return $maxDepth;
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace GraphQL\Tests\Validator;
use GraphQL\FormattedError;
use GraphQL\Language\Parser;
use GraphQL\Type\Introspection;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\AbstractQuerySecurity;
abstract class AbstractQuerySecurityTest extends \PHPUnit_Framework_TestCase
{
/**
* @param $max
*
* @return AbstractQuerySecurity
*/
abstract protected function getRule($max);
/**
* @param $max
* @param $count
*
* @return string
*/
abstract protected function getErrorMessage($max, $count);
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage argument must be greater or equal to 0.
*/
public function testMaxQueryDepthMustBeGreaterOrEqualTo0()
{
$this->getRule(-1);
}
protected function createFormattedError($max, $count, $locations = [])
{
return FormattedError::create($this->getErrorMessage($max, $count), $locations);
}
protected function assertDocumentValidator($queryString, $max, array $expectedErrors = [])
{
$errors = DocumentValidator::validate(
QuerySecuritySchema::buildSchema(),
Parser::parse($queryString),
[$this->getRule($max)]
);
$this->assertEquals($expectedErrors, array_map(['GraphQL\Error', 'formatError'], $errors), $queryString);
return $errors;
}
protected function assertIntrospectionQuery($maxExpected)
{
$query = Introspection::getIntrospectionQuery(true);
$this->assertMaxValue($query, $maxExpected);
}
protected function assertIntrospectionTypeMetaFieldQuery($maxExpected)
{
$query = '
{
__type(name: "Human") {
name
}
}
';
$this->assertMaxValue($query, $maxExpected);
}
protected function assertTypeNameMetaFieldQuery($maxExpected)
{
$query = '
{
human {
__typename
firstName
}
}
';
$this->assertMaxValue($query, $maxExpected);
}
protected function assertMaxValue($query, $maxExpected)
{
$this->assertDocumentValidator($query, $maxExpected);
$newMax = $maxExpected - 1;
if ($newMax !== AbstractQuerySecurity::DISABLED) {
$this->assertDocumentValidator($query, $newMax, [$this->createFormattedError($newMax, $maxExpected)]);
}
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace GraphQL\Tests\Validator;
use GraphQL\Validator\Rules\QueryComplexity;
class QueryComplexityTest extends AbstractQuerySecurityTest
{
/** @var QueryComplexity */
private static $rule;
/**
* @param $max
* @param $count
*
* @return string
*/
protected function getErrorMessage($max, $count)
{
return QueryComplexity::maxQueryComplexityErrorMessage($max, $count);
}
/**
* @param $maxDepth
*
* @return QueryComplexity
*/
protected function getRule($maxDepth = null)
{
if (null === self::$rule) {
self::$rule = new QueryComplexity($maxDepth);
} elseif (null !== $maxDepth) {
self::$rule->setMaxQueryComplexity($maxDepth);
}
return self::$rule;
}
public function testSimpleQueries()
{
$query = 'query MyQuery { human { firstName } }';
$this->assertDocumentValidators($query, 2, 3);
}
public function testInlineFragmentQueries()
{
$query = 'query MyQuery { human { ... on Human { firstName } } }';
$this->assertDocumentValidators($query, 2, 3);
}
public function testFragmentQueries()
{
$query = 'query MyQuery { human { ...F1 } } fragment F1 on Human { firstName}';
$this->assertDocumentValidators($query, 2, 3);
}
public function testAliasesQueries()
{
$query = 'query MyQuery { thomas: human(name: "Thomas") { firstName } jeremy: human(name: "Jeremy") { firstName } }';
$this->assertDocumentValidators($query, 4, 5);
}
public function testCustomComplexityQueries()
{
$query = 'query MyQuery { human { dogs { name } } }';
$this->assertDocumentValidators($query, 12, 13);
}
public function testCustomComplexityWithArgsQueries()
{
$query = 'query MyQuery { human { dogs(name: "Root") { name } } }';
$this->assertDocumentValidators($query, 3, 4);
}
public function testCustomComplexityWithVariablesQueries()
{
$query = 'query MyQuery($dog: String!) { human { dogs(name: $dog) { name } } }';
$this->getRule()->setRawVariableValues(['dog' => 'Roots']);
$this->assertDocumentValidators($query, 3, 4);
}
public function testComplexityIntrospectionQuery()
{
$this->assertIntrospectionQuery(109);
}
public function testIntrospectionTypeMetaFieldQuery()
{
$this->assertIntrospectionTypeMetaFieldQuery(2);
}
public function testTypeNameMetaFieldQuery()
{
$this->assertTypeNameMetaFieldQuery(3);
}
private function assertDocumentValidators($query, $queryComplexity, $startComplexity)
{
for ($maxComplexity = $startComplexity; $maxComplexity >= 0; --$maxComplexity) {
$positions = [];
if ($maxComplexity < $queryComplexity && $maxComplexity !== QueryComplexity::DISABLED) {
$positions = [$this->createFormattedError($maxComplexity, $queryComplexity)];
}
$this->assertDocumentValidator($query, $maxComplexity, $positions);
}
}
}

View File

@ -0,0 +1,148 @@
<?php
namespace GraphQL\Tests\Validator;
use GraphQL\Validator\Rules\QueryDepth;
class QueryDepthTest extends AbstractQuerySecurityTest
{
/**
* @param $max
* @param $count
*
* @return string
*/
protected function getErrorMessage($max, $count)
{
return QueryDepth::maxQueryDepthErrorMessage($max, $count);
}
/**
* @param $maxDepth
*
* @return QueryDepth
*/
protected function getRule($maxDepth)
{
return new QueryDepth($maxDepth);
}
/**
* @param $queryDepth
* @param int $maxQueryDepth
* @param array $expectedErrors
* @dataProvider queryDataProvider
*/
public function testSimpleQueries($queryDepth, $maxQueryDepth = 7, $expectedErrors = [])
{
$this->assertDocumentValidator($this->buildRecursiveQuery($queryDepth), $maxQueryDepth, $expectedErrors);
}
/**
* @param $queryDepth
* @param int $maxQueryDepth
* @param array $expectedErrors
* @dataProvider queryDataProvider
*/
public function testFragmentQueries($queryDepth, $maxQueryDepth = 7, $expectedErrors = [])
{
$this->assertDocumentValidator($this->buildRecursiveUsingFragmentQuery($queryDepth), $maxQueryDepth, $expectedErrors);
}
/**
* @param $queryDepth
* @param int $maxQueryDepth
* @param array $expectedErrors
* @dataProvider queryDataProvider
*/
public function testInlineFragmentQueries($queryDepth, $maxQueryDepth = 7, $expectedErrors = [])
{
$this->assertDocumentValidator($this->buildRecursiveUsingInlineFragmentQuery($queryDepth), $maxQueryDepth, $expectedErrors);
}
public function testComplexityIntrospectionQuery()
{
$this->assertIntrospectionQuery(7);
}
public function testIntrospectionTypeMetaFieldQuery()
{
$this->assertIntrospectionTypeMetaFieldQuery(1);
}
public function testTypeNameMetaFieldQuery()
{
$this->assertTypeNameMetaFieldQuery(1);
}
public function queryDataProvider()
{
return [
[1], // Valid because depth under default limit (7)
[2],
[3],
[4],
[5],
[6],
[7],
[8, 9], // Valid because depth under new limit (9)
[10, 0], // Valid because 0 depth disable limit
[
10,
8,
[$this->createFormattedError(8, 10)],
], // failed because depth over limit (8)
[
20,
15,
[$this->createFormattedError(15, 20)],
], // failed because depth over limit (15)
];
}
private function buildRecursiveQuery($depth)
{
$query = sprintf('query MyQuery { human%s }', $this->buildRecursiveQueryPart($depth));
return $query;
}
private function buildRecursiveUsingFragmentQuery($depth)
{
$query = sprintf(
'query MyQuery { human { ...F1 } } fragment F1 on Human %s',
$this->buildRecursiveQueryPart($depth)
);
return $query;
}
private function buildRecursiveUsingInlineFragmentQuery($depth)
{
$query = sprintf(
'query MyQuery { human { ...on Human %s } }',
$this->buildRecursiveQueryPart($depth)
);
return $query;
}
private function buildRecursiveQueryPart($depth)
{
$templates = [
'human' => ' { firstName%s } ',
'dog' => ' dogs { name%s } ',
];
$part = $templates['human'];
for ($i = 1; $i <= $depth; ++$i) {
$key = ($i % 2 == 1) ? 'human' : 'dog';
$template = $templates[$key];
$part = sprintf($part, ('human' == $key ? ' owner ' : '').$template);
}
$part = str_replace('%s', '', $part);
return $part;
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace GraphQL\Tests\Validator;
use GraphQL\Schema;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
class QuerySecuritySchema
{
private static $schema;
private static $dogType;
private static $humanType;
private static $queryRootType;
/**
* @return Schema
*/
public static function buildSchema()
{
if (null !== self::$schema) {
return self::$schema;
}
self::$schema = new Schema(static::buildQueryRootType());
return self::$schema;
}
public static function buildQueryRootType()
{
if (null !== self::$queryRootType) {
return self::$queryRootType;
}
self::$queryRootType = new ObjectType([
'name' => 'QueryRoot',
'fields' => [
'human' => [
'type' => self::buildHumanType(),
'args' => ['name' => ['type' => Type::string()]],
],
],
]);
return self::$queryRootType;
}
public static function buildHumanType()
{
if (null !== self::$humanType) {
return self::$humanType;
}
self::$humanType = new ObjectType(
[
'name' => 'Human',
'fields' => [
'firstName' => ['type' => Type::nonNull(Type::string())],
'dogs' => [
'type' => function () {
return Type::nonNull(
Type::listOf(
Type::nonNull(self::buildDogType())
)
);
},
'complexity' => function ($childrenComplexity, $args) {
$complexity = isset($args['name']) ? 1 : 10;
return $childrenComplexity + $complexity;
},
'args' => ['name' => ['type' => Type::string()]],
],
],
]
);
return self::$humanType;
}
public static function buildDogType()
{
if (null !== self::$dogType) {
return self::$dogType;
}
self::$dogType = new ObjectType(
[
'name' => 'Dog',
'fields' => [
'name' => ['type' => Type::nonNull(Type::string())],
'master' => [
'type' => self::buildHumanType(),
],
],
]
);
return self::$dogType;
}
}