Skip to content

Commit e09e79e

Browse files
committed
AC-1120: Turn off the input limit for RESTful endpoints by default and create a CLI command to turn them on
1 parent 5788e74 commit e09e79e

File tree

15 files changed

+716
-31
lines changed

15 files changed

+716
-31
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0"?>
2+
<!--
3+
/**
4+
* Representation of Webapi module in System Configuration (Magento admin panel).
5+
*
6+
* Copyright © Magento, Inc. All rights reserved.
7+
* See COPYING.txt for license details.
8+
*/
9+
-->
10+
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
11+
<system>
12+
<section id="graphql" translate="label" type="text" sortOrder="103" showInDefault="1" showInWebsite="1" showInStore="1">
13+
<label>Magento GraphQl</label>
14+
<tab>service</tab>
15+
<resource>Magento_GraphQl::config_graphql</resource>
16+
<group id="validation" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
17+
<label>Input Limits</label>
18+
<field id="input_limit_enabled" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1">
19+
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
20+
<label>Enable Input Limits</label>
21+
</field>
22+
<field id="maximum_page_size" translate="label comment" type="text" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="1">
23+
<label>Maximum Page Size</label>
24+
<comment>Maximum number of items allowed in a paginated search result.</comment>
25+
<depends>
26+
<field id="input_limit_enabled">1</field>
27+
</depends>
28+
</field>
29+
</group>
30+
</section>
31+
</system>
32+
</config>
33+

app/code/Magento/Webapi/etc/adminhtml/system.xml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,31 @@
2020
<comment>If empty, UTF-8 will be used.</comment>
2121
</field>
2222
</group>
23+
<group id="validation" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
24+
<label>Input Limits</label>
25+
<field id="input_limit_enabled" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" showInStore="1">
26+
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
27+
<label>Enable Input Limits</label>
28+
</field>
29+
<field id="complex_array_limit" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
30+
<label>Complex Array Limit</label>
31+
<comment>Maximum number of items allowed in an entity's array property.</comment>
32+
<depends>
33+
<field id="input_limit_enabled">1</field>
34+
</depends>
35+
</field>
36+
<field id="maximum_page_size" translate="label comment" type="text" sortOrder="15" showInDefault="1" showInWebsite="1" showInStore="1">
37+
<label>Maximum Page Size</label>
38+
<comment>Maximum number of items allowed in a paginated search result.</comment>
39+
<depends>
40+
<field id="input_limit_enabled">1</field>
41+
</depends>
42+
</field>
43+
<field id="default_page_size" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
44+
<label>Default Page Size</label>
45+
<comment>Default number of items a paginated search result.</comment>
46+
</field>
47+
</group>
2348
</section>
2449
</system>
2550
</config>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Framework\GraphQl\Query\Resolver\Argument\Validator;
10+
11+
use Magento\Framework\App\Config\ScopeConfigInterface;
12+
use Magento\Store\Model\ScopeInterface;
13+
14+
/**
15+
* Provides configuration related to the GraphQL input limit validation
16+
*/
17+
class ConfigProvider
18+
{
19+
/**
20+
* Path to the configuration setting for if the input limiting is enabled
21+
*/
22+
public const CONFIG_PATH_INPUT_LIMIT_ENABLED = 'graphql/validation/input_limit_enabled';
23+
24+
/**
25+
* Path to the configuration setting for maximum page size allowed
26+
*/
27+
public const CONFIG_PATH_MAXIMUM_PAGE_SIZE = 'graphql/validation/maximum_page_size';
28+
29+
/**
30+
* @var ScopeConfigInterface
31+
*/
32+
private $scopeConfig;
33+
34+
/**
35+
* @param ScopeConfigInterface $scopeConfig
36+
*/
37+
public function __construct(ScopeConfigInterface $scopeConfig)
38+
{
39+
$this->scopeConfig = $scopeConfig;
40+
}
41+
42+
/**
43+
* @inheritDoc
44+
*/
45+
public function isInputLimitingEnabled(): bool
46+
{
47+
return $this->scopeConfig->isSetFlag(
48+
self::CONFIG_PATH_INPUT_LIMIT_ENABLED,
49+
ScopeInterface::SCOPE_STORE
50+
);
51+
}
52+
53+
/**
54+
* @inheritDoc
55+
*/
56+
public function getMaximumPageSize(): ?int
57+
{
58+
$value = $this->scopeConfig->getValue(
59+
self::CONFIG_PATH_MAXIMUM_PAGE_SIZE,
60+
ScopeInterface::SCOPE_STORE
61+
);
62+
return isset($value) ? (int)$value : null;
63+
}
64+
}

lib/internal/Magento/Framework/GraphQl/Query/Resolver/Argument/Validator/SearchCriteriaValidator.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
namespace Magento\Framework\GraphQl\Query\Resolver\Argument\Validator;
1010

11+
use Magento\Framework\App\ObjectManager;
1112
use Magento\Framework\GraphQl\Config\Element\Field;
1213
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
1314
use Magento\Framework\GraphQl\Query\Resolver\Argument\ValidatorInterface;
@@ -22,22 +23,36 @@ class SearchCriteriaValidator implements ValidatorInterface
2223
*/
2324
private $maxPageSize;
2425

26+
/**
27+
* @var ConfigProvider|null
28+
*/
29+
private $configProvider;
30+
2531
/**
2632
* @param int $maxPageSize
33+
* @param ConfigProvider|null $configProvider
2734
*/
28-
public function __construct(int $maxPageSize)
35+
public function __construct(int $maxPageSize, ?ConfigProvider $configProvider = null)
2936
{
3037
$this->maxPageSize = $maxPageSize;
38+
$this->configProvider = $configProvider ?? ObjectManager::getInstance()
39+
->get(ConfigProvider::class);
3140
}
3241

3342
/**
3443
* @inheritDoc
3544
*/
3645
public function validate(Field $field, $args): void
3746
{
38-
if (isset($args['pageSize']) && $args['pageSize'] > $this->maxPageSize) {
47+
if (!$this->configProvider->isInputLimitingEnabled()) {
48+
return;
49+
}
50+
51+
$max = $this->configProvider->getMaximumPageSize() ?? $this->maxPageSize;
52+
53+
if (isset($args['pageSize']) && $args['pageSize'] > $max) {
3954
throw new GraphQlInputException(
40-
__("Maximum pageSize is %max", ['max' => $this->maxPageSize])
55+
__("Maximum pageSize is %max", ['max' => $max])
4156
);
4257
}
4358
}

lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/Resolver/Argument/Validator/SearchCriteriaValidatorTest.php

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,102 @@
1010

1111
use Magento\Framework\GraphQl\Config\Element\Field;
1212
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
13+
use Magento\Framework\GraphQl\Query\Resolver\Argument\Validator\ConfigProvider;
1314
use Magento\Framework\GraphQl\Query\Resolver\Argument\Validator\SearchCriteriaValidator;
15+
use PHPUnit\Framework\MockObject\MockObject;
1416
use PHPUnit\Framework\TestCase;
1517

1618
/**
1719
* Verify behavior of graphql search criteria validator
1820
*/
1921
class SearchCriteriaValidatorTest extends TestCase
2022
{
23+
/**
24+
* @var ConfigProvider|MockObject
25+
*/
26+
private $configProvider;
27+
28+
/**
29+
* @var SearchCriteriaValidator
30+
*/
31+
private $validator;
32+
33+
protected function setUp(): void
34+
{
35+
$this->configProvider = self::getMockBuilder(ConfigProvider::class)
36+
->disableOriginalConstructor()
37+
->getMock();
38+
$this->validator = new SearchCriteriaValidator(3, $this->configProvider);
39+
}
40+
41+
/**
42+
* @doesNotPerformAssertions
43+
*/
44+
public function testValidValueWithDisabledDefaultConfig()
45+
{
46+
$this->configProvider->method('isInputLimitingEnabled')
47+
->willReturn(false);
48+
$field = self::getMockBuilder(Field::class)
49+
->disableOriginalConstructor()
50+
->getMock();
51+
$this->validator->validate($field, ['pageSize' => 50]);
52+
}
53+
2154
/**
2255
* @doesNotPerformAssertions
2356
*/
2457
public function testValidValue()
2558
{
26-
$validator = new SearchCriteriaValidator(3);
59+
$this->configProvider->method('isInputLimitingEnabled')
60+
->willReturn(false);
2761
$field = self::getMockBuilder(Field::class)
2862
->disableOriginalConstructor()
2963
->getMock();
30-
$validator->validate($field, ['pageSize' => 3]);
64+
$this->validator->validate($field, ['pageSize' => 3]);
3165
}
3266

33-
public function testValidInvalidMaxValue()
67+
/**
68+
* @doesNotPerformAssertions
69+
*/
70+
public function testValidValueWithConfig()
71+
{
72+
$this->configProvider->method('isInputLimitingEnabled')
73+
->willReturn(true);
74+
$this->configProvider->method('getMaximumPageSize')
75+
->willReturn(10);
76+
77+
$field = self::getMockBuilder(Field::class)
78+
->disableOriginalConstructor()
79+
->getMock();
80+
$this->validator->validate($field, ['pageSize' => 10]);
81+
}
82+
83+
public function testInvalidMaxValue()
3484
{
3585
$this->expectException(GraphQlInputException::class);
3686
$this->expectExceptionMessage("Maximum pageSize is 3");
37-
$validator = new SearchCriteriaValidator(3);
87+
88+
$this->configProvider->method('isInputLimitingEnabled')
89+
->willReturn(true);
90+
$field = self::getMockBuilder(Field::class)
91+
->disableOriginalConstructor()
92+
->getMock();
93+
$this->validator->validate($field, ['pageSize' => 4]);
94+
}
95+
96+
public function testInvalidValueWithConfig()
97+
{
98+
$this->expectException(GraphQlInputException::class);
99+
$this->expectExceptionMessage("Maximum pageSize is 10");
100+
101+
$this->configProvider->method('isInputLimitingEnabled')
102+
->willReturn(true);
103+
$this->configProvider->method('getMaximumPageSize')
104+
->willReturn(10);
105+
38106
$field = self::getMockBuilder(Field::class)
39107
->disableOriginalConstructor()
40108
->getMock();
41-
$validator->validate($field, ['pageSize' => 4]);
109+
$this->validator->validate($field, ['pageSize' => 11]);
42110
}
43111
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Framework\Webapi\InputLimit;
10+
11+
use Magento\Framework\Api\SearchCriteriaInterface;
12+
use Magento\Framework\Webapi\Validator\ConfigProvider;
13+
14+
/**
15+
* Sets the default page size with the configured input limits
16+
*/
17+
class DefaultPageSizeSetter
18+
{
19+
/**
20+
* @var ConfigProvider
21+
*/
22+
private $validationConfigProvider;
23+
24+
/**
25+
* @param ConfigProvider $validationConfigProvider
26+
*/
27+
public function __construct(ConfigProvider $validationConfigProvider)
28+
{
29+
$this->validationConfigProvider = $validationConfigProvider;
30+
}
31+
32+
/**
33+
* Set the default page size if needed using the optional parameter as a fallback value
34+
*
35+
* @param SearchCriteriaInterface $searchCriteria The search criteria to manipulate
36+
* @param int|null $defaultPageSizeFallback The fallback value if limiting is enabled but a limit has not been set
37+
*/
38+
public function processSearchCriteria(
39+
SearchCriteriaInterface $searchCriteria,
40+
int $defaultPageSizeFallback = null
41+
): void {
42+
if ($searchCriteria->getPageSize() === null
43+
&& $this->validationConfigProvider->isInputLimitingEnabled()
44+
) {
45+
$searchCriteria->setPageSize(
46+
$this->validationConfigProvider->getDefaultPageSize() ?? $defaultPageSizeFallback
47+
);
48+
}
49+
}
50+
}

lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Magento\Framework\Webapi\Exception as WebapiException;
2525
use Magento\Framework\Webapi\CustomAttribute\PreprocessorInterface;
2626
use Laminas\Code\Reflection\ClassReflection;
27+
use Magento\Framework\Webapi\InputLimit\DefaultPageSizeSetter;
2728
use Magento\Framework\Webapi\Validator\ServiceInputValidatorInterface;
2829

2930
/**
@@ -97,6 +98,11 @@ class ServiceInputProcessor implements ServicePayloadConverterInterface
9798
*/
9899
private $defaultPageSize;
99100

101+
/**
102+
* @var DefaultPageSizeSetter|null
103+
*/
104+
private $defaultPageSizeSetter;
105+
100106
/**
101107
* Initialize dependencies.
102108
*
@@ -110,6 +116,8 @@ class ServiceInputProcessor implements ServicePayloadConverterInterface
110116
* @param array $customAttributePreprocessors
111117
* @param ServiceInputValidatorInterface|null $serviceInputValidator
112118
* @param int $defaultPageSize
119+
* @param DefaultPageSizeSetter|null $defaultPageSizeSetter
120+
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
113121
*/
114122
public function __construct(
115123
TypeProcessor $typeProcessor,
@@ -121,7 +129,8 @@ public function __construct(
121129
ConfigInterface $config = null,
122130
array $customAttributePreprocessors = [],
123131
ServiceInputValidatorInterface $serviceInputValidator = null,
124-
int $defaultPageSize = 20
132+
int $defaultPageSize = 20,
133+
?DefaultPageSizeSetter $defaultPageSizeSetter = null
125134
) {
126135
$this->typeProcessor = $typeProcessor;
127136
$this->objectManager = $objectManager;
@@ -136,6 +145,8 @@ public function __construct(
136145
$this->serviceInputValidator = $serviceInputValidator
137146
?: ObjectManager::getInstance()->get(ServiceInputValidatorInterface::class);
138147
$this->defaultPageSize = $defaultPageSize >= 10 ? $defaultPageSize : 10;
148+
$this->defaultPageSizeSetter = $defaultPageSizeSetter ?? ObjectManager::getInstance()
149+
->get(DefaultPageSizeSetter::class);
139150
}
140151

141152
/**
@@ -309,10 +320,8 @@ protected function _createFromArray($className, $data)
309320
}
310321
}
311322

312-
if ($object instanceof SearchCriteriaInterface
313-
&& $object->getPageSize() === null
314-
) {
315-
$object->setPageSize($this->defaultPageSize);
323+
if ($object instanceof SearchCriteriaInterface) {
324+
$this->defaultPageSizeSetter->processSearchCriteria($object, $this->defaultPageSize);
316325
}
317326

318327
return $object;

0 commit comments

Comments
 (0)