diff --git a/app/code/Magento/Analytics/Plugin/BearerTokenValidatorPlugin.php b/app/code/Magento/Analytics/Plugin/BearerTokenValidatorPlugin.php new file mode 100644 index 0000000000000..3497d2a14fd25 --- /dev/null +++ b/app/code/Magento/Analytics/Plugin/BearerTokenValidatorPlugin.php @@ -0,0 +1,49 @@ +config = $config; + } + + /*** + * Always allow access token for analytics to be used as bearer + * + * @param BearerTokenValidator $subject + * @param bool $result + * @param Integration $integration + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterIsIntegrationAllowedAsBearerToken( + BearerTokenValidator $subject, + bool $result, + Integration $integration + ): bool { + return $result || $integration->getName() === $this->config->getValue('analytics/integration_name'); + } +} diff --git a/app/code/Magento/Analytics/Test/Unit/Plugin/BearerTokenValidatorPluginTest.php b/app/code/Magento/Analytics/Test/Unit/Plugin/BearerTokenValidatorPluginTest.php new file mode 100644 index 0000000000000..94b5a7817838c --- /dev/null +++ b/app/code/Magento/Analytics/Test/Unit/Plugin/BearerTokenValidatorPluginTest.php @@ -0,0 +1,72 @@ +createMock(ScopeConfigInterface::class); + $config->method('getValue') + ->with('analytics/integration_name') + ->willReturn('abc'); + $this->plugin = new BearerTokenValidatorPlugin($config); + $this->validator = $this->createMock(BearerTokenValidator::class); + } + + public function testTrueIsPassedThrough() + { + $integration = $this->createMock(Integration::class); + $integration->method('__call') + ->with('getName') + ->willReturn('invalid'); + + $result = $this->plugin->afterIsIntegrationAllowedAsBearerToken($this->validator, true, $integration); + self::assertTrue($result); + } + + public function testFalseWhenIntegrationDoesntMatch() + { + $integration = $this->createMock(Integration::class); + $integration->method('__call') + ->with('getName') + ->willReturn('invalid'); + + $result = $this->plugin->afterIsIntegrationAllowedAsBearerToken($this->validator, false, $integration); + self::assertFalse($result); + } + + public function testTrueWhenIntegrationMatches() + { + $integration = $this->createMock(Integration::class); + $integration->method('__call') + ->with('getName') + ->willReturn('abc'); + + $result = $this->plugin->afterIsIntegrationAllowedAsBearerToken($this->validator, true, $integration); + self::assertTrue($result); + } +} diff --git a/app/code/Magento/Analytics/etc/di.xml b/app/code/Magento/Analytics/etc/di.xml index 0a57676b5fb8f..3615bd9ca2d32 100644 --- a/app/code/Magento/Analytics/etc/di.xml +++ b/app/code/Magento/Analytics/etc/di.xml @@ -271,4 +271,7 @@ Magento\Framework\Model\ResourceModel\Type\Db\ConnectionFactory + + + diff --git a/app/code/Magento/Integration/Model/CompositeTokenReader.php b/app/code/Magento/Integration/Model/CompositeTokenReader.php new file mode 100644 index 0000000000000..0c53cf76b779b --- /dev/null +++ b/app/code/Magento/Integration/Model/CompositeTokenReader.php @@ -0,0 +1,48 @@ +readers = $readers; + } + + /** + * @inheritDoc + */ + public function read(string $token): UserToken + { + foreach ($this->readers as $reader) { + try { + return $reader->read($token); + } catch (UserTokenException $exception) { + continue; + } + } + + throw new UserTokenException('Composite reader could not read a token'); + } +} diff --git a/app/code/Magento/Integration/Model/Config/AuthorizationConfig.php b/app/code/Magento/Integration/Model/Config/AuthorizationConfig.php new file mode 100644 index 0000000000000..7cbd4ee3e8306 --- /dev/null +++ b/app/code/Magento/Integration/Model/Config/AuthorizationConfig.php @@ -0,0 +1,49 @@ +scopeConfig = $scopeConfig; + } + + /** + * Return if integration access tokens can be used as bearer tokens + * + * @return bool + */ + public function isIntegrationAsBearerEnabled(): bool + { + return $this->scopeConfig->isSetFlag( + self::CONFIG_PATH_INTEGRATION_BEARER, + ScopeInterface::SCOPE_STORE + ); + } +} diff --git a/app/code/Magento/Integration/Model/OpaqueToken/Reader.php b/app/code/Magento/Integration/Model/OpaqueToken/Reader.php index fedf613188840..c0b318a563a27 100644 --- a/app/code/Magento/Integration/Model/OpaqueToken/Reader.php +++ b/app/code/Magento/Integration/Model/OpaqueToken/Reader.php @@ -8,14 +8,24 @@ namespace Magento\Integration\Model\OpaqueToken; +use Magento\Authorization\Model\UserContextInterface; +use Magento\Framework\App\ObjectManager; use Magento\Integration\Api\Data\UserToken; use Magento\Integration\Api\Exception\UserTokenException; +use Magento\Integration\Api\IntegrationServiceInterface; use Magento\Integration\Api\UserTokenReaderInterface; +use Magento\Integration\Model\Config\AuthorizationConfig; use Magento\Integration\Model\CustomUserContext; use Magento\Integration\Model\Oauth\Token; use Magento\Integration\Model\Oauth\TokenFactory; use Magento\Integration\Helper\Oauth\Data as OauthHelper; +use Magento\Integration\Model\Validator\BearerTokenValidator; +/** + * Reads user token data + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class Reader implements UserTokenReaderInterface { /** @@ -28,14 +38,34 @@ class Reader implements UserTokenReaderInterface */ private $helper; + /** + * @var IntegrationServiceInterface + */ + private IntegrationServiceInterface $integrationService; + + /** + * @var BearerTokenValidator + */ + private BearerTokenValidator $bearerTokenValidator; + /** * @param TokenFactory $tokenFactory * @param OauthHelper $helper + * @param IntegrationServiceInterface|null $integrationService + * @param BearerTokenValidator|null $bearerTokenValidator */ - public function __construct(TokenFactory $tokenFactory, OauthHelper $helper) - { + public function __construct( + TokenFactory $tokenFactory, + OauthHelper $helper, + ?IntegrationServiceInterface $integrationService = null, + ?BearerTokenValidator $bearerTokenValidator = null + ) { $this->tokenFactory = $tokenFactory; $this->helper = $helper; + $this->integrationService = $integrationService ?? ObjectManager::getInstance() + ->get(IntegrationServiceInterface::class); + $this->bearerTokenValidator = $bearerTokenValidator ?? ObjectManager::getInstance() + ->get(BearerTokenValidator::class); } /** @@ -43,7 +73,32 @@ public function __construct(TokenFactory $tokenFactory, OauthHelper $helper) */ public function read(string $token): UserToken { - /** @var Token $tokenModel */ + + $tokenModel = $this->getTokenModel($token); + $userType = (int) $tokenModel->getUserType(); + $this->validateUserType($userType); + $userId = $this->getUserId($tokenModel); + + $issued = \DateTimeImmutable::createFromFormat( + 'Y-m-d H:i:s', + $tokenModel->getCreatedAt(), + new \DateTimeZone('UTC') + ); + $lifetimeHours = $userType === CustomUserContext::USER_TYPE_ADMIN + ? $this->helper->getAdminTokenLifetime() : $this->helper->getCustomerTokenLifetime(); + $expires = $issued->add(new \DateInterval("PT{$lifetimeHours}H")); + + return new UserToken(new CustomUserContext((int) $userId, (int) $userType), new Data($issued, $expires)); + } + + /** + * Create the token model from the input + * + * @param string $token + * @return Token + */ + private function getTokenModel(string $token): Token + { $tokenModel = $this->tokenFactory->create(); $tokenModel = $tokenModel->load($token, 'token'); @@ -53,27 +108,51 @@ public function read(string $token): UserToken if ($tokenModel->getRevoked()) { throw new UserTokenException('Token was revoked'); } - $userType = (int) $tokenModel->getUserType(); - if ($userType !== CustomUserContext::USER_TYPE_ADMIN && $userType !== CustomUserContext::USER_TYPE_CUSTOMER) { + + return $tokenModel; + } + + /** + * Validate the given user type + * + * @param int $userType + * @throws UserTokenException + */ + private function validateUserType(int $userType): void + { + if ($userType !== CustomUserContext::USER_TYPE_ADMIN + && $userType !== CustomUserContext::USER_TYPE_CUSTOMER + && $userType !== CustomUserContext::USER_TYPE_INTEGRATION + ) { throw new UserTokenException('Invalid token found'); } + } + + /** + * Determine the user id for a given token + * + * @param Token $tokenModel + * @return int + */ + private function getUserId(Token $tokenModel): int + { + $userType = (int)$tokenModel->getUserType(); + $userId = null; + if ($userType === CustomUserContext::USER_TYPE_ADMIN) { $userId = $tokenModel->getAdminId(); + } elseif ($userType === CustomUserContext::USER_TYPE_INTEGRATION) { + $integration = $this->integrationService->findByConsumerId($tokenModel->getConsumerId()); + if ($this->bearerTokenValidator->isIntegrationAllowedAsBearerToken($integration)) { + $userId = $integration->getId(); + } } else { $userId = $tokenModel->getCustomerId(); } if (!$userId) { throw new UserTokenException('Invalid token found'); } - $issued = \DateTimeImmutable::createFromFormat( - 'Y-m-d H:i:s', - $tokenModel->getCreatedAt(), - new \DateTimeZone('UTC') - ); - $lifetimeHours = $userType === CustomUserContext::USER_TYPE_ADMIN - ? $this->helper->getAdminTokenLifetime() : $this->helper->getCustomerTokenLifetime(); - $expires = $issued->add(new \DateInterval("PT{$lifetimeHours}H")); - return new UserToken(new CustomUserContext((int) $userId, (int) $userType), new Data($issued, $expires)); + return (int)$userId; } } diff --git a/app/code/Magento/Integration/Model/UserToken/ExpirationValidator.php b/app/code/Magento/Integration/Model/UserToken/ExpirationValidator.php index 3e19fed38aead..b22f146d6c64e 100644 --- a/app/code/Magento/Integration/Model/UserToken/ExpirationValidator.php +++ b/app/code/Magento/Integration/Model/UserToken/ExpirationValidator.php @@ -8,11 +8,15 @@ namespace Magento\Integration\Model\UserToken; +use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\Exception\AuthorizationException; use Magento\Integration\Api\Data\UserToken; use Magento\Integration\Api\UserTokenValidatorInterface; use Magento\Framework\Stdlib\DateTime\DateTime as DtUtil; +/** + * Validates if a token is expired + */ class ExpirationValidator implements UserTokenValidatorInterface { /** @@ -33,8 +37,30 @@ public function __construct(DtUtil $datetimeUtil) */ public function validate(UserToken $token): void { - if ($token->getData()->getExpires()->getTimestamp() <= $this->datetimeUtil->gmtTimestamp()) { + if (!$this->isIntegrationToken($token) && $this->isTokenExpired($token)) { throw new AuthorizationException(__('Consumer key has expired')); } } + + /** + * Check if a token is expired + * + * @param UserToken $token + * @return bool + */ + private function isTokenExpired(UserToken $token): bool + { + return $token->getData()->getExpires()->getTimestamp() <= $this->datetimeUtil->gmtTimestamp(); + } + + /** + * Check if a token is an integration token + * + * @param UserToken $token + * @return bool + */ + private function isIntegrationToken(UserToken $token): bool + { + return $token->getUserContext()->getUserType() === UserContextInterface::USER_TYPE_INTEGRATION; + } } diff --git a/app/code/Magento/Integration/Model/Validator/BearerTokenValidator.php b/app/code/Magento/Integration/Model/Validator/BearerTokenValidator.php new file mode 100644 index 0000000000000..a18cc64d891a0 --- /dev/null +++ b/app/code/Magento/Integration/Model/Validator/BearerTokenValidator.php @@ -0,0 +1,43 @@ +authorizationConfig = $authorizationConfig; + } + + /** + * Validate an integration's access token can be used as a standalone bearer token + * + * @param Integration $integration + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function isIntegrationAllowedAsBearerToken(Integration $integration): bool + { + return $this->authorizationConfig->isIntegrationAsBearerEnabled(); + } +} diff --git a/app/code/Magento/Integration/Test/Unit/Model/CompositeTokenReaderTest.php b/app/code/Magento/Integration/Test/Unit/Model/CompositeTokenReaderTest.php new file mode 100644 index 0000000000000..44f80e6a59dd9 --- /dev/null +++ b/app/code/Magento/Integration/Test/Unit/Model/CompositeTokenReaderTest.php @@ -0,0 +1,69 @@ +createMock(UserToken::class); + $reader1 = $this->createMock(UserTokenReaderInterface::class); + $reader1->method('read') + ->with('abc') + ->willReturn($token1); + + $token2 = $this->createMock(UserToken::class); + $reader2 = $this->createMock(UserTokenReaderInterface::class); + $reader2->method('read') + ->with('abc') + ->willReturn($token2); + + $composite = new CompositeTokenReader([$reader1, $reader2]); + + self::assertSame($token1, $composite->read('abc')); + } + + public function testCompositeReaderReturnsNextTokenOnError() + { + $reader1 = $this->createMock(UserTokenReaderInterface::class); + $reader1->method('read') + ->with('abc') + ->willThrowException(new UserTokenException('Fail')); + + $token2 = $this->createMock(UserToken::class); + $reader2 = $this->createMock(UserTokenReaderInterface::class); + $reader2->method('read') + ->with('abc') + ->willReturn($token2); + + $composite = new CompositeTokenReader([$reader1, $reader1, $reader2]); + + self::assertSame($token2, $composite->read('abc')); + } + + public function testCompositeReaderFailsWhenNoTokensFound() + { + $this->expectExceptionMessage('Composite reader could not read a token'); + $this->expectException(UserTokenException::class); + + $reader1 = $this->createMock(UserTokenReaderInterface::class); + $reader1->method('read') + ->with('abc') + ->willThrowException(new UserTokenException('Fail')); + + $composite = new CompositeTokenReader([$reader1, $reader1, $reader1]); + $composite->read('abc'); + } +} diff --git a/app/code/Magento/Integration/Test/Unit/Model/Config/AuthorizationConfigTest.php b/app/code/Magento/Integration/Test/Unit/Model/Config/AuthorizationConfigTest.php new file mode 100644 index 0000000000000..96832b3c5d1a5 --- /dev/null +++ b/app/code/Magento/Integration/Test/Unit/Model/Config/AuthorizationConfigTest.php @@ -0,0 +1,51 @@ +scopeConfig = $this->createMock(ScopeConfigInterface::class); + $this->config = new AuthorizationConfig($this->scopeConfig); + } + + public function testEnabled() + { + $this->scopeConfig->method('isSetFlag') + ->with('oauth/consumer/enable_integration_as_bearer') + ->willReturn(true); + + self::assertTrue($this->config->isIntegrationAsBearerEnabled()); + } + + public function testDisabled() + { + $this->scopeConfig->method('isSetFlag') + ->with('oauth/consumer/enable_integration_as_bearer') + ->willReturn(false); + + self::assertFalse($this->config->isIntegrationAsBearerEnabled()); + } +} diff --git a/app/code/Magento/Integration/Test/Unit/Model/UserToken/ExpirationValidatorTest.php b/app/code/Magento/Integration/Test/Unit/Model/UserToken/ExpirationValidatorTest.php index 6bc1dad3087b0..9751f9bfa3d8e 100644 --- a/app/code/Magento/Integration/Test/Unit/Model/UserToken/ExpirationValidatorTest.php +++ b/app/code/Magento/Integration/Test/Unit/Model/UserToken/ExpirationValidatorTest.php @@ -8,6 +8,7 @@ namespace Magento\Integration\Test\Unit\Model\UserToken; +use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\Exception\AuthorizationException; use Magento\Integration\Api\Data\UserToken; use Magento\Integration\Api\Data\UserTokenDataInterface; @@ -62,10 +63,22 @@ public function getUserTokens(): array ->willReturn(\DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2021-04-07 16:00:00')); $futureToken->method('getData')->willReturn($futureData); + $integrationToken = $this->createMock(UserToken::class); + $userContext = $this->createMock(UserContextInterface::class); + $userContext->method('getUserType') + ->willReturn(UserContextInterface::USER_TYPE_INTEGRATION); + $integrationData = $this->createMock(UserTokenDataInterface::class); + $integrationData->method('getExpires') + ->willReturn(\DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2021-04-07 12:00:00')); + $integrationToken->method('getData')->willReturn($pastData); + $integrationToken->method('getUserContext') + ->willReturn($userContext); + return [ 'past' => [$pastToken, false, $currentTs], 'exact' => [$exactToken, false, $currentTs], - 'future' => [$futureToken, true, $currentTs] + 'future' => [$futureToken, true, $currentTs], + 'integration' => [$integrationToken, true, $currentTs], ]; } diff --git a/app/code/Magento/Integration/etc/adminhtml/system.xml b/app/code/Magento/Integration/etc/adminhtml/system.xml index 3d465a9642805..2db4c9a7e5412 100644 --- a/app/code/Magento/Integration/etc/adminhtml/system.xml +++ b/app/code/Magento/Integration/etc/adminhtml/system.xml @@ -54,6 +54,10 @@ Timeout for OAuth consumer credentials Post request within X seconds. required-entry validate-zero-or-greater validate-number + + Magento\Config\Model\Config\Source\Yesno + + diff --git a/app/code/Magento/Integration/etc/di.xml b/app/code/Magento/Integration/etc/di.xml index 9fe53af49f073..57430e302cac7 100644 --- a/app/code/Magento/Integration/etc/di.xml +++ b/app/code/Magento/Integration/etc/di.xml @@ -45,6 +45,13 @@ - + + + + + Magento\Integration\Model\OpaqueToken\Reader + + + diff --git a/app/code/Magento/JwtUserToken/etc/di.xml b/app/code/Magento/JwtUserToken/etc/di.xml index faa523aef1027..c70e34a8a31a3 100644 --- a/app/code/Magento/JwtUserToken/etc/di.xml +++ b/app/code/Magento/JwtUserToken/etc/di.xml @@ -7,7 +7,7 @@ --> - + @@ -24,4 +24,11 @@ + + + + Magento\JwtUserToken\Model\Reader + + + diff --git a/app/code/Magento/SalesRule/view/frontend/web/template/payment/discount.html b/app/code/Magento/SalesRule/view/frontend/web/template/payment/discount.html index 649c1ba4bfe59..f7cf894445a12 100644 --- a/app/code/Magento/SalesRule/view/frontend/web/template/payment/discount.html +++ b/app/code/Magento/SalesRule/view/frontend/web/template/payment/discount.html @@ -44,10 +44,10 @@ + + + - - - diff --git a/app/code/Magento/Webapi/Controller/Rest/InputParamsResolver.php b/app/code/Magento/Webapi/Controller/Rest/InputParamsResolver.php index b5588a9a49a2f..5197a56b3a7f1 100644 --- a/app/code/Magento/Webapi/Controller/Rest/InputParamsResolver.php +++ b/app/code/Magento/Webapi/Controller/Rest/InputParamsResolver.php @@ -4,18 +4,25 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Webapi\Controller\Rest; use Magento\Framework\Api\SimpleDataObjectConverter; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Reflection\MethodsMap; use Magento\Framework\Webapi\Exception; use Magento\Framework\Webapi\ServiceInputProcessor; use Magento\Framework\Webapi\Rest\Request as RestRequest; +use Magento\Framework\Webapi\Validator\EntityArrayValidator\InputArraySizeLimitValue; use Magento\Webapi\Controller\Rest\Router\Route; /** * This class is responsible for retrieving resolved input data + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class InputParamsResolver { @@ -54,6 +61,11 @@ class InputParamsResolver */ private $methodsMap; + /** + * @var InputArraySizeLimitValue + */ + private $inputArraySizeLimitValue; + /** * Initialize dependencies * @@ -63,6 +75,7 @@ class InputParamsResolver * @param Router $router * @param RequestValidator $requestValidator * @param MethodsMap|null $methodsMap + * @param InputArraySizeLimitValue|null $inputArraySizeLimitValue */ public function __construct( RestRequest $request, @@ -70,7 +83,8 @@ public function __construct( ServiceInputProcessor $serviceInputProcessor, Router $router, RequestValidator $requestValidator, - MethodsMap $methodsMap = null + MethodsMap $methodsMap = null, + ?InputArraySizeLimitValue $inputArraySizeLimitValue = null ) { $this->request = $request; $this->paramsOverrider = $paramsOverrider; @@ -79,29 +93,34 @@ public function __construct( $this->requestValidator = $requestValidator; $this->methodsMap = $methodsMap ?: ObjectManager::getInstance() ->get(MethodsMap::class); + $this->inputArraySizeLimitValue = $inputArraySizeLimitValue ?? ObjectManager::getInstance() + ->get(InputArraySizeLimitValue::class); } /** * Process and resolve input parameters * * @return array - * @throws Exception + * @throws Exception|AuthorizationException|LocalizedException */ public function resolve() { $this->requestValidator->validate(); $route = $this->getRoute(); - $serviceMethodName = $route->getServiceMethod(); - $serviceClassName = $route->getServiceClass(); - $inputData = $this->getInputData(); + $this->inputArraySizeLimitValue->set($route->getInputArraySizeLimit()); - return $this->serviceInputProcessor->process($serviceClassName, $serviceMethodName, $inputData); + return $this->serviceInputProcessor->process( + $route->getServiceClass(), + $route->getServiceMethod(), + $this->getInputData(), + ); } /** * Get API input data * * @return array + * @throws InputException|Exception */ public function getInputData() { @@ -131,6 +150,7 @@ public function getInputData() * Retrieve current route. * * @return Route + * @throws Exception */ public function getRoute() { @@ -174,9 +194,9 @@ private function validateParameters( } } if (!empty($paramOverriders)) { - throw new \UnexpectedValueException( - __('The current request does not expect the next parameters: ' . implode(', ', $paramOverriders)) - ); + $message = 'The current request does not expect the next parameters: ' + . implode(', ', $paramOverriders); + throw new \UnexpectedValueException(__($message)->__toString()); } } } diff --git a/app/code/Magento/Webapi/Controller/Rest/Router/Route.php b/app/code/Magento/Webapi/Controller/Rest/Router/Route.php index ca5dfe2e2b822..d346e1cb46951 100644 --- a/app/code/Magento/Webapi/Controller/Rest/Router/Route.php +++ b/app/code/Magento/Webapi/Controller/Rest/Router/Route.php @@ -1,15 +1,19 @@ route; } + + /** + * Get array size limit of input data + * + * @return int|null + */ + public function getInputArraySizeLimit(): ?int + { + return $this->inputArraySizeLimit; + } + + /** + * Set array size limit of input data + * + * @param int|null $limit + */ + public function setInputArraySizeLimit(?int $limit): void + { + $this->inputArraySizeLimit = $limit; + } } diff --git a/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php b/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php index a01c5054f9b5f..2c54b21624811 100644 --- a/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php +++ b/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php @@ -4,18 +4,23 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Webapi\Controller\Soap\Request; +use InvalidArgumentException; use Magento\Framework\Api\ExtensibleDataInterface; use Magento\Framework\Api\MetadataObjectInterface; use Magento\Framework\Api\SimpleDataObjectConverter; use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Webapi\Authorization; use Magento\Framework\Exception\AuthorizationException; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Framework\Webapi\ServiceInputProcessor; -use Magento\Framework\Webapi\Request as SoapRequest; +use Magento\Framework\Webapi\Request as WebapiRequest; use Magento\Framework\Webapi\Exception as WebapiException; +use Magento\Framework\Webapi\Validator\EntityArrayValidator\InputArraySizeLimitValue; use Magento\Webapi\Controller\Rest\ParamsOverrider; use Magento\Webapi\Model\Soap\Config as SoapConfig; use Magento\Framework\Reflection\MethodsMap; @@ -30,45 +35,45 @@ */ class Handler { - const RESULT_NODE_NAME = 'result'; + public const RESULT_NODE_NAME = 'result'; /** - * @var \Magento\Framework\Webapi\Request + * @var WebapiRequest */ protected $_request; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ protected $_objectManager; /** - * @var \Magento\Webapi\Model\Soap\Config + * @var SoapConfig */ protected $_apiConfig; /** - * @var \Magento\Framework\Webapi\Authorization + * @var Authorization */ protected $authorization; /** - * @var \Magento\Framework\Api\SimpleDataObjectConverter + * @var SimpleDataObjectConverter */ protected $_dataObjectConverter; /** - * @var \Magento\Framework\Webapi\ServiceInputProcessor + * @var ServiceInputProcessor */ protected $serviceInputProcessor; /** - * @var \Magento\Framework\Reflection\DataObjectProcessor + * @var DataObjectProcessor */ protected $_dataObjectProcessor; /** - * @var \Magento\Framework\Reflection\MethodsMap + * @var MethodsMap */ protected $methodsMapProcessor; @@ -77,11 +82,16 @@ class Handler */ private $paramsOverrider; + /** + * @var InputArraySizeLimitValue + */ + private $inputArraySizeLimitValue; + /** * Initialize dependencies. * - * @param SoapRequest $request - * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param WebapiRequest $request + * @param ObjectManagerInterface $objectManager * @param SoapConfig $apiConfig * @param Authorization $authorization * @param SimpleDataObjectConverter $dataObjectConverter @@ -89,17 +99,20 @@ class Handler * @param DataObjectProcessor $dataObjectProcessor * @param MethodsMap $methodsMapProcessor * @param ParamsOverrider|null $paramsOverrider + * @param InputArraySizeLimitValue|null $inputArraySizeLimitValue + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - SoapRequest $request, - \Magento\Framework\ObjectManagerInterface $objectManager, + WebapiRequest $request, + ObjectManagerInterface $objectManager, SoapConfig $apiConfig, Authorization $authorization, SimpleDataObjectConverter $dataObjectConverter, ServiceInputProcessor $serviceInputProcessor, DataObjectProcessor $dataObjectProcessor, MethodsMap $methodsMapProcessor, - ?ParamsOverrider $paramsOverrider = null + ?ParamsOverrider $paramsOverrider = null, + ?InputArraySizeLimitValue $inputArraySizeLimitValue = null ) { $this->_request = $request; $this->_objectManager = $objectManager; @@ -110,6 +123,8 @@ public function __construct( $this->_dataObjectProcessor = $dataObjectProcessor; $this->methodsMapProcessor = $methodsMapProcessor; $this->paramsOverrider = $paramsOverrider ?? ObjectManager::getInstance()->get(ParamsOverrider::class); + $this->inputArraySizeLimitValue = $inputArraySizeLimitValue ?? ObjectManager::getInstance() + ->get(InputArraySizeLimitValue::class); } /** @@ -144,10 +159,24 @@ public function __call($operation, $arguments) } $service = $this->_objectManager->get($serviceClass); $inputData = $this->prepareOperationInput($serviceClass, $serviceMethodInfo, $arguments); - $outputData = call_user_func_array([$service, $serviceMethod], $inputData); + $outputData = $this->runServiceMethod($service, $serviceMethod, $inputData); return $this->_prepareResponseData($outputData, $serviceClass, $serviceMethod); } + /** + * Runs service method + * + * @param object $service + * @param string $serviceMethod + * @param array $inputData + * @return false|mixed + */ + private function runServiceMethod($service, $serviceMethod, $inputData) + { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return call_user_func_array([$service, $serviceMethod], $inputData); + } + /** * Convert arguments received from SOAP server to arguments to pass to a service. * @@ -156,7 +185,6 @@ public function __call($operation, $arguments) * @param array $arguments * @return array * @throws WebapiException - * @throws \Magento\Framework\Exception\InputException */ private function prepareOperationInput(string $serviceClass, array $methodMetadata, array $arguments): array { @@ -164,6 +192,7 @@ private function prepareOperationInput(string $serviceClass, array $methodMetada $arguments = reset($arguments); $arguments = $this->_dataObjectConverter->convertStdObjectToArray($arguments, true); $arguments = $this->paramsOverrider->override($arguments, $methodMetadata[ServiceMetadata::KEY_ROUTE_PARAMS]); + $this->inputArraySizeLimitValue->set($methodMetadata[ServiceMetadata::KEY_INPUT_ARRAY_SIZE_LIMIT]); return $this->serviceInputProcessor->process( $serviceClass, @@ -179,8 +208,9 @@ private function prepareOperationInput(string $serviceClass, array $methodMetada * @param string $serviceMethod * @param array $arguments * @return array - * @deprecated 100.3.2 + * @throws WebapiException * @see Handler::prepareOperationInput() + * @deprecated 100.3.2 */ protected function _prepareRequestData($serviceClass, $serviceMethod, $arguments) { @@ -198,7 +228,7 @@ protected function _prepareRequestData($serviceClass, $serviceMethod, $arguments * @param string $serviceClassName * @param string $serviceMethodName * @return array - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ protected function _prepareResponseData($data, $serviceClassName, $serviceMethodName) { @@ -225,7 +255,7 @@ protected function _prepareResponseData($data, $serviceClassName, $serviceMethod } elseif (is_scalar($data) || $data === null) { $result = $data; } else { - throw new \InvalidArgumentException("Service returned result in invalid format."); + throw new InvalidArgumentException("Service returned result in invalid format."); } return [self::RESULT_NODE_NAME => $result]; } diff --git a/app/code/Magento/Webapi/Model/Authorization/SoapUserContext.php b/app/code/Magento/Webapi/Model/Authorization/SoapUserContext.php index 78de066ac31ee..d225f0d96546e 100644 --- a/app/code/Magento/Webapi/Model/Authorization/SoapUserContext.php +++ b/app/code/Magento/Webapi/Model/Authorization/SoapUserContext.php @@ -12,9 +12,7 @@ use Magento\Integration\Model\Oauth\TokenFactory; use Magento\Integration\Api\IntegrationServiceInterface; use Magento\Framework\Webapi\Request; -use Magento\Framework\Stdlib\DateTime\DateTime as Date; -use Magento\Framework\Stdlib\DateTime; -use Magento\Integration\Helper\Oauth\Data as OauthHelper; +use Magento\Integration\Model\Validator\BearerTokenValidator; /** * SOAP specific user context based on opaque tokens. @@ -49,7 +47,12 @@ class SoapUserContext implements UserContextInterface /** * @var IntegrationServiceInterface */ - private $integrationService; + private IntegrationServiceInterface $integrationService; + + /** + * @var BearerTokenValidator + */ + private BearerTokenValidator $bearerTokenValidator; /** * Initialize dependencies. @@ -57,18 +60,19 @@ class SoapUserContext implements UserContextInterface * @param Request $request * @param TokenFactory $tokenFactory * @param IntegrationServiceInterface $integrationService - * @param DateTime|null $dateTime - * @param Date|null $date - * @param OauthHelper|null $oauthHelper + * @param BearerTokenValidator|null $bearerTokenValidator */ public function __construct( Request $request, TokenFactory $tokenFactory, - IntegrationServiceInterface $integrationService + IntegrationServiceInterface $integrationService, + ?BearerTokenValidator $bearerTokenValidator = null ) { $this->request = $request; $this->tokenFactory = $tokenFactory; $this->integrationService = $integrationService; + $this->bearerTokenValidator = $bearerTokenValidator ?? ObjectManager::getInstance() + ->get(BearerTokenValidator::class); } /** @@ -114,6 +118,7 @@ private function processRequest() //phpcs:ignore CopyPaste $this->isRequestProcessed = true; return; } + $bearerToken = $headerPieces[1]; /** @var Token $token */ @@ -123,8 +128,11 @@ private function processRequest() //phpcs:ignore CopyPaste return; } if (((int) $token->getUserType()) === UserContextInterface::USER_TYPE_INTEGRATION) { - $this->userId = $this->integrationService->findByConsumerId($token->getConsumerId())->getId(); - $this->userType = UserContextInterface::USER_TYPE_INTEGRATION; + $integration = $this->integrationService->findByConsumerId($token->getConsumerId()); + if ($this->bearerTokenValidator->isIntegrationAllowedAsBearerToken($integration)) { + $this->userId = $integration->getId(); + $this->userType = UserContextInterface::USER_TYPE_INTEGRATION; + } } $this->isRequestProcessed = true; } diff --git a/app/code/Magento/Webapi/Model/Config/Converter.php b/app/code/Magento/Webapi/Model/Config/Converter.php index b05b1a25b3dc4..9ee9d745c9a45 100644 --- a/app/code/Magento/Webapi/Model/Config/Converter.php +++ b/app/code/Magento/Webapi/Model/Config/Converter.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Webapi\Model\Config; /** @@ -13,22 +16,23 @@ class Converter implements \Magento\Framework\Config\ConverterInterface /**#@+ * Array keys for config internal representation. */ - const KEY_SERVICE_CLASS = 'class'; - const KEY_URL = 'url'; - const KEY_SERVICE_METHOD = 'method'; - const KEY_SECURE = 'secure'; - const KEY_ROUTES = 'routes'; - const KEY_ACL_RESOURCES = 'resources'; - const KEY_SERVICE = 'service'; - const KEY_SERVICES = 'services'; - const KEY_FORCE = 'force'; - const KEY_VALUE = 'value'; - const KEY_DATA_PARAMETERS = 'parameters'; - const KEY_SOURCE = 'source'; - const KEY_METHOD = 'method'; - const KEY_METHODS = 'methods'; - const KEY_DESCRIPTION = 'description'; - const KEY_REAL_SERVICE_METHOD = 'realMethod'; + public const KEY_SERVICE_CLASS = 'class'; + public const KEY_URL = 'url'; + public const KEY_SERVICE_METHOD = 'method'; + public const KEY_SECURE = 'secure'; + public const KEY_ROUTES = 'routes'; + public const KEY_ACL_RESOURCES = 'resources'; + public const KEY_SERVICE = 'service'; + public const KEY_SERVICES = 'services'; + public const KEY_FORCE = 'force'; + public const KEY_VALUE = 'value'; + public const KEY_DATA_PARAMETERS = 'parameters'; + public const KEY_SOURCE = 'source'; + public const KEY_METHOD = 'method'; + public const KEY_METHODS = 'methods'; + public const KEY_DESCRIPTION = 'description'; + public const KEY_REAL_SERVICE_METHOD = 'realMethod'; + public const KEY_INPUT_ARRAY_SIZE_LIMIT = 'input-array-size-limit'; /**#@-*/ /** @@ -96,6 +100,8 @@ public function convert($source) $secureNode = $route->attributes->getNamedItem('secure'); $secure = $secureNode ? (bool)trim($secureNode->nodeValue) : false; + $arraySizeLimit = $this->getInputArraySizeLimit($route); + // We could handle merging here by checking if the route already exists $result[self::KEY_ROUTES][$url][$method] = [ self::KEY_SECURE => $secure, @@ -105,6 +111,7 @@ public function convert($source) ], self::KEY_ACL_RESOURCES => $resourceReferences, self::KEY_DATA_PARAMETERS => $data, + self::KEY_INPUT_ARRAY_SIZE_LIMIT => $arraySizeLimit, ]; $serviceSecure = false; @@ -114,6 +121,9 @@ public function convert($source) if (!isset($serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_REAL_SERVICE_METHOD])) { $serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_REAL_SERVICE_METHOD] = $serviceMethod; } + if (!isset($serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_INPUT_ARRAY_SIZE_LIMIT])) { + $serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_INPUT_ARRAY_SIZE_LIMIT] = $arraySizeLimit; + } $serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_SECURE] = $serviceSecure || $secure; $serviceClassData[self::KEY_METHODS][$soapMethod][self::KEY_DATA_PARAMETERS] = $serviceData; @@ -169,4 +179,26 @@ protected function convertVersion($url) { return substr($url, 1, strpos($url, '/', 1)-1); } + + /** + * Returns array size limit of input data + * + * @param \DOMElement $routeDOMElement + * @return int|null + */ + private function getInputArraySizeLimit(\DOMElement $routeDOMElement): ?int + { + /** @var \DOMElement $dataDOMElement */ + foreach ($routeDOMElement->getElementsByTagName('data') as $dataDOMElement) { + if ($dataDOMElement->nodeType === XML_ELEMENT_NODE) { + $inputArraySizeLimitDOMNode = $dataDOMElement->attributes + ->getNamedItem(self::KEY_INPUT_ARRAY_SIZE_LIMIT); + return ($inputArraySizeLimitDOMNode instanceof \DOMNode) + ? (int)$inputArraySizeLimitDOMNode->nodeValue + : null; + } + } + + return null; + } } diff --git a/app/code/Magento/Webapi/Model/Rest/Config.php b/app/code/Magento/Webapi/Model/Rest/Config.php index d572b9c21cfc9..59a7f45eb8abe 100644 --- a/app/code/Magento/Webapi/Model/Rest/Config.php +++ b/app/code/Magento/Webapi/Model/Rest/Config.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Webapi\Model\Rest; use Magento\Webapi\Controller\Rest\Router\Route; @@ -17,25 +20,28 @@ class Config /**#@+ * HTTP methods supported by REST. */ - const HTTP_METHOD_GET = 'GET'; - const HTTP_METHOD_DELETE = 'DELETE'; - const HTTP_METHOD_PUT = 'PUT'; - const HTTP_METHOD_POST = 'POST'; - const HTTP_METHOD_PATCH = 'PATCH'; + public const HTTP_METHOD_GET = 'GET'; + public const HTTP_METHOD_DELETE = 'DELETE'; + public const HTTP_METHOD_PUT = 'PUT'; + public const HTTP_METHOD_POST = 'POST'; + public const HTTP_METHOD_PATCH = 'PATCH'; /**#@-*/ /**#@+ * Keys that a used for config internal representation. */ - const KEY_IS_SECURE = 'isSecure'; - const KEY_CLASS = 'class'; - const KEY_METHOD = 'method'; - const KEY_ROUTE_PATH = 'routePath'; - const KEY_ACL_RESOURCES = 'resources'; - const KEY_PARAMETERS = 'parameters'; + public const KEY_IS_SECURE = 'isSecure'; + public const KEY_CLASS = 'class'; + public const KEY_METHOD = 'method'; + public const KEY_ROUTE_PATH = 'routePath'; + public const KEY_ACL_RESOURCES = 'resources'; + public const KEY_PARAMETERS = 'parameters'; + public const KEY_INPUT_ARRAY_SIZE_LIMIT = 'input-array-size-limit'; /*#@-*/ - /*#@-*/ + /** + * @var ModelConfigInterface + */ protected $_config; /** @@ -79,7 +85,8 @@ protected function _createRoute($routeData) ->setServiceMethod($routeData[self::KEY_METHOD]) ->setSecure($routeData[self::KEY_IS_SECURE]) ->setAclResources($routeData[self::KEY_ACL_RESOURCES]) - ->setParameters($routeData[self::KEY_PARAMETERS]); + ->setParameters($routeData[self::KEY_PARAMETERS]) + ->setInputArraySizeLimit($routeData[self::KEY_INPUT_ARRAY_SIZE_LIMIT]); return $route; } @@ -120,6 +127,7 @@ public function getRestRoutes(\Magento\Framework\Webapi\Rest\Request $request) self::KEY_IS_SECURE => $methodInfo[Converter::KEY_SECURE], self::KEY_ACL_RESOURCES => array_keys($methodInfo[Converter::KEY_ACL_RESOURCES]), self::KEY_PARAMETERS => $methodInfo[Converter::KEY_DATA_PARAMETERS], + self::KEY_INPUT_ARRAY_SIZE_LIMIT => $methodInfo[Converter::KEY_INPUT_ARRAY_SIZE_LIMIT], ] ); return $routes; @@ -143,6 +151,7 @@ public function getRestRoutes(\Magento\Framework\Webapi\Rest\Request $request) self::KEY_IS_SECURE => $methodInfo[Converter::KEY_SECURE], self::KEY_ACL_RESOURCES => $aclResources, self::KEY_PARAMETERS => $methodInfo[Converter::KEY_DATA_PARAMETERS], + self::KEY_INPUT_ARRAY_SIZE_LIMIT => $methodInfo[Converter::KEY_INPUT_ARRAY_SIZE_LIMIT], ] ); } diff --git a/app/code/Magento/Webapi/Model/ServiceMetadata.php b/app/code/Magento/Webapi/Model/ServiceMetadata.php index 36f5819b03c98..fe44f7814b161 100644 --- a/app/code/Magento/Webapi/Model/ServiceMetadata.php +++ b/app/code/Magento/Webapi/Model/ServiceMetadata.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Webapi\Model; use Magento\Framework\App\ObjectManager; @@ -18,35 +21,39 @@ class ServiceMetadata /**#@+ * Keys that a used for service config internal representation. */ - const KEY_CLASS = 'class'; + public const KEY_CLASS = 'class'; - const KEY_IS_SECURE = 'isSecure'; + public const KEY_IS_SECURE = 'isSecure'; - const KEY_SERVICE_METHODS = 'methods'; + public const KEY_SERVICE_METHODS = 'methods'; - const KEY_METHOD = 'method'; + public const KEY_METHOD = 'method'; - const KEY_IS_REQUIRED = 'inputRequired'; + public const KEY_IS_REQUIRED = 'inputRequired'; - const KEY_ACL_RESOURCES = 'resources'; + public const KEY_ACL_RESOURCES = 'resources'; - const KEY_ROUTES = 'routes'; + public const KEY_ROUTES = 'routes'; - const KEY_ROUTE_METHOD = 'method'; + public const KEY_ROUTE_METHOD = 'method'; - const KEY_ROUTE_PARAMS = 'parameters'; + public const KEY_ROUTE_PARAMS = 'parameters'; - const KEY_METHOD_ALIAS = 'methodAlias'; + public const KEY_METHOD_ALIAS = 'methodAlias'; - const SERVICES_CONFIG_CACHE_ID = 'services-services-config'; + public const KEY_INPUT_ARRAY_SIZE_LIMIT = 'input-array-size-limit'; - const ROUTES_CONFIG_CACHE_ID = 'routes-services-config'; + public const SERVICES_CONFIG_CACHE_ID = 'services-services-config'; - const REFLECTED_TYPES_CACHE_ID = 'soap-reflected-types'; + public const ROUTES_CONFIG_CACHE_ID = 'routes-services-config'; - /**#@-*/ + public const REFLECTED_TYPES_CACHE_ID = 'soap-reflected-types'; /**#@-*/ + + /** + * @var array + */ protected $services; /** @@ -123,7 +130,8 @@ protected function initServicesMetadata() self::KEY_IS_SECURE => $methodMetadata[Converter::KEY_SECURE], self::KEY_ACL_RESOURCES => $methodMetadata[Converter::KEY_ACL_RESOURCES], self::KEY_METHOD_ALIAS => $methodName, - self::KEY_ROUTE_PARAMS => $methodMetadata[Converter::KEY_DATA_PARAMETERS] + self::KEY_ROUTE_PARAMS => $methodMetadata[Converter::KEY_DATA_PARAMETERS], + self::KEY_INPUT_ARRAY_SIZE_LIMIT => $methodMetadata[Converter::KEY_INPUT_ARRAY_SIZE_LIMIT], ]; $services[$serviceName][self::KEY_CLASS] = $serviceClass; $methods[] = $methodMetadata[Converter::KEY_REAL_SERVICE_METHOD]; @@ -134,6 +142,7 @@ protected function initServicesMetadata() $methods ); foreach ($services[$serviceName][self::KEY_SERVICE_METHODS] as $methodName => &$methodMetadata) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $methodMetadata = array_merge( $methodMetadata, $reflectedMethodsMetadata[$methodMetadata[self::KEY_METHOD]] @@ -311,9 +320,11 @@ protected function initRoutesMetadata() $version = explode('/', ltrim($url, '/'))[0]; $serviceName = $this->getServiceName($serviceClass, $version); $methodName = $data[Converter::KEY_SERVICE][Converter::KEY_METHOD]; + $limit = $data[Converter::KEY_INPUT_ARRAY_SIZE_LIMIT]; $routes[$serviceName][self::KEY_ROUTES][$url][$method][self::KEY_ROUTE_METHOD] = $methodName; $routes[$serviceName][self::KEY_ROUTES][$url][$method][self::KEY_ROUTE_PARAMS] = $data[Converter::KEY_DATA_PARAMETERS]; + $routes[$serviceName][self::KEY_ROUTES][$url][$method][self::KEY_INPUT_ARRAY_SIZE_LIMIT] = $limit; } } return $routes; diff --git a/app/code/Magento/Webapi/Model/Soap/Config.php b/app/code/Magento/Webapi/Model/Soap/Config.php index 190280ff8f004..54fa76b43cb8d 100644 --- a/app/code/Magento/Webapi/Model/Soap/Config.php +++ b/app/code/Magento/Webapi/Model/Soap/Config.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Webapi\Model\Soap; use Magento\Webapi\Model\ServiceMetadata; @@ -77,12 +80,14 @@ protected function getSoapOperations($requestedServices) $class = $serviceData[ServiceMetadata::KEY_CLASS]; $operation = $methodData[ServiceMetadata::KEY_METHOD_ALIAS]; $operationName = $serviceName . ucfirst($operation); + $inputArraySizeLimit = $methodData[ServiceMetadata::KEY_INPUT_ARRAY_SIZE_LIMIT]; $this->soapOperations[$operationName] = [ ServiceMetadata::KEY_CLASS => $class, ServiceMetadata::KEY_METHOD => $method, ServiceMetadata::KEY_IS_SECURE => $methodData[ServiceMetadata::KEY_IS_SECURE], ServiceMetadata::KEY_ACL_RESOURCES => $methodData[ServiceMetadata::KEY_ACL_RESOURCES], - ServiceMetadata::KEY_ROUTE_PARAMS => $methodData[ServiceMetadata::KEY_ROUTE_PARAMS] + ServiceMetadata::KEY_ROUTE_PARAMS => $methodData[ServiceMetadata::KEY_ROUTE_PARAMS], + ServiceMetadata::KEY_INPUT_ARRAY_SIZE_LIMIT => $inputArraySizeLimit, ]; } } @@ -108,12 +113,15 @@ public function getServiceMethodInfo($soapOperation, $requestedServices) \Magento\Framework\Webapi\Exception::HTTP_NOT_FOUND ); } + $inputArraySizeLimit = $soapOperations[$soapOperation][ServiceMetadata::KEY_INPUT_ARRAY_SIZE_LIMIT]; + return [ ServiceMetadata::KEY_CLASS => $soapOperations[$soapOperation][ServiceMetadata::KEY_CLASS], ServiceMetadata::KEY_METHOD => $soapOperations[$soapOperation][ServiceMetadata::KEY_METHOD], ServiceMetadata::KEY_IS_SECURE => $soapOperations[$soapOperation][ServiceMetadata::KEY_IS_SECURE], ServiceMetadata::KEY_ACL_RESOURCES => $soapOperations[$soapOperation][ServiceMetadata::KEY_ACL_RESOURCES], - ServiceMetadata::KEY_ROUTE_PARAMS => $soapOperations[$soapOperation][ServiceMetadata::KEY_ROUTE_PARAMS] + ServiceMetadata::KEY_ROUTE_PARAMS => $soapOperations[$soapOperation][ServiceMetadata::KEY_ROUTE_PARAMS], + ServiceMetadata::KEY_INPUT_ARRAY_SIZE_LIMIT => $inputArraySizeLimit, ]; } diff --git a/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.php b/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.php index bd8fcc78953a4..02daff32d2be6 100644 --- a/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.php +++ b/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.php @@ -17,7 +17,8 @@ ], 'secure' => false, 'realMethod' => 'getById', - 'parameters' => [] + 'parameters' => [], + 'input-array-size-limit' => null, ], 'save' => [ 'resources' => [ @@ -25,7 +26,8 @@ ], 'secure' => false, 'realMethod' => 'save', - 'parameters' => [] + 'parameters' => [], + 'input-array-size-limit' => 50, ], 'saveSelf' => [ 'resources' => [ @@ -39,6 +41,7 @@ 'value' => null, ], ], + 'input-array-size-limit' => null, ], 'deleteById' => [ 'resources' => [ @@ -47,7 +50,8 @@ ], 'secure' => false, 'realMethod' => 'deleteById', - 'parameters' => [] + 'parameters' => [], + 'input-array-size-limit' => null, ], ], ], @@ -70,6 +74,7 @@ 'value' => '%customer_id%', ], ], + 'input-array-size-limit' => null, ], ], '/V1/customers/me' => [ @@ -88,6 +93,7 @@ 'value' => null, ], ], + 'input-array-size-limit' => null, ], 'PUT' => [ 'secure' => true, @@ -104,6 +110,7 @@ 'value' => null, ], ], + 'input-array-size-limit' => null, ], ], '/V1/customers' => [ @@ -116,8 +123,8 @@ 'resources' => [ 'Magento_Customer::manage' => true, ], - 'parameters' => [ - ], + 'parameters' => [], + 'input-array-size-limit' => 50, ], ], '/V1/customers/:id' => [ @@ -130,8 +137,8 @@ 'resources' => [ 'Magento_Customer::read' => true, ], - 'parameters' => [ - ], + 'parameters' => [], + 'input-array-size-limit' => null, ], 'DELETE' => [ 'secure' => false, @@ -143,8 +150,8 @@ 'Magento_Customer::manage' => true, 'Magento_Customer::delete' => true, ], - 'parameters' => [ - ], + 'parameters' => [], + 'input-array-size-limit' => null, ], ], ], diff --git a/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.xml b/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.xml index 50b9abb8f17ae..5872a8e94bccf 100644 --- a/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.xml +++ b/app/code/Magento/Webapi/Test/Unit/Model/Config/_files/webapi.xml @@ -39,6 +39,7 @@ + diff --git a/app/code/Magento/Webapi/etc/webapi_base.xsd b/app/code/Magento/Webapi/etc/webapi_base.xsd index 7d1a5a14ba78f..77ef3f78324c8 100644 --- a/app/code/Magento/Webapi/etc/webapi_base.xsd +++ b/app/code/Magento/Webapi/etc/webapi_base.xsd @@ -55,8 +55,9 @@ - + + diff --git a/app/code/Magento/Webapi/etc/webapi_soap/di.xml b/app/code/Magento/Webapi/etc/webapi_soap/di.xml index f6fef5498aa76..7433e73794283 100644 --- a/app/code/Magento/Webapi/etc/webapi_soap/di.xml +++ b/app/code/Magento/Webapi/etc/webapi_soap/di.xml @@ -6,22 +6,21 @@ */ --> - - - Magento\Webapi\Controller\Soap\Request - - - - Magento\Webapi\Model\Authorization\SoapUserContext - 9 + + Magento\Webapi\Model\Authorization\OauthUserContext + 5 Magento\Webapi\Model\Authorization\TokenUserContext 10 + + Magento\Webapi\Model\Authorization\SoapUserContext + 15 + Magento\Webapi\Model\Authorization\GuestUserContext 100 diff --git a/app/code/Magento/WebapiAsync/Controller/Rest/Asynchronous/InputParamsResolver.php b/app/code/Magento/WebapiAsync/Controller/Rest/Asynchronous/InputParamsResolver.php index 064bd99b9b6bf..8601e5011bda7 100644 --- a/app/code/Magento/WebapiAsync/Controller/Rest/Asynchronous/InputParamsResolver.php +++ b/app/code/Magento/WebapiAsync/Controller/Rest/Asynchronous/InputParamsResolver.php @@ -8,12 +8,19 @@ namespace Magento\WebapiAsync\Controller\Rest\Asynchronous; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Webapi\Exception; use Magento\Framework\Webapi\Rest\Request as RestRequest; use Magento\Framework\Webapi\ServiceInputProcessor; +use Magento\Framework\Webapi\Validator\EntityArrayValidator\InputArraySizeLimitValue; use Magento\Webapi\Controller\Rest\InputParamsResolver as WebapiInputParamsResolver; use Magento\Webapi\Controller\Rest\ParamsOverrider; use Magento\Webapi\Controller\Rest\RequestValidator; use Magento\Webapi\Controller\Rest\Router; +use Magento\Webapi\Controller\Rest\Router\Route; /** * This class is responsible for retrieving resolved input data @@ -41,7 +48,7 @@ class InputParamsResolver */ private $requestValidator; /** - * @var \Magento\Webapi\Controller\Rest\InputParamsResolver + * @var WebapiInputParamsResolver */ private $inputParamsResolver; /** @@ -49,16 +56,22 @@ class InputParamsResolver */ private $isBulk; + /** + * @var InputArraySizeLimitValue|null + */ + private $inputArraySizeLimitValue; + /** * Initialize dependencies. * - * @param \Magento\Framework\Webapi\Rest\Request $request - * @param \Magento\Webapi\Controller\Rest\ParamsOverrider $paramsOverrider - * @param \Magento\Framework\Webapi\ServiceInputProcessor $inputProcessor - * @param \Magento\Webapi\Controller\Rest\Router $router - * @param \Magento\Webapi\Controller\Rest\RequestValidator $requestValidator - * @param \Magento\Webapi\Controller\Rest\InputParamsResolver $inputParamsResolver + * @param RestRequest $request + * @param ParamsOverrider $paramsOverrider + * @param ServiceInputProcessor $inputProcessor + * @param Router $router + * @param RequestValidator $requestValidator + * @param WebapiInputParamsResolver $inputParamsResolver * @param bool $isBulk + * @param InputArraySizeLimitValue|null $inputArraySizeLimitValue */ public function __construct( RestRequest $request, @@ -67,7 +80,8 @@ public function __construct( Router $router, RequestValidator $requestValidator, WebapiInputParamsResolver $inputParamsResolver, - $isBulk = false + bool $isBulk = false, + ?InputArraySizeLimitValue $inputArraySizeLimitValue = null ) { $this->request = $request; $this->paramsOverrider = $paramsOverrider; @@ -76,6 +90,8 @@ public function __construct( $this->requestValidator = $requestValidator; $this->inputParamsResolver = $inputParamsResolver; $this->isBulk = $isBulk; + $this->inputArraySizeLimitValue = $inputArraySizeLimitValue ?? ObjectManager::getInstance() + ->get(InputArraySizeLimitValue::class); } /** @@ -85,20 +101,31 @@ public function __construct( * or throw \Exception if at least one request entity params is not valid * * @return array - * @throws \Magento\Framework\Exception\InputException if no value is provided for required parameters - * @throws \Magento\Framework\Webapi\Exception - * @throws \Magento\Framework\Exception\AuthorizationException + * @throws InputException if no value is provided for required parameters + * @throws Exception + * @throws AuthorizationException|LocalizedException */ public function resolve() { if ($this->isBulk === false) { return [$this->inputParamsResolver->resolve()]; } + $this->requestValidator->validate(); $webapiResolvedParams = []; + $route = $this->getRoute(); + $routeServiceClass = $route->getServiceClass(); + $routeServiceMethod = $route->getServiceMethod(); + $this->inputArraySizeLimitValue->set($route->getInputArraySizeLimit()); + foreach ($this->getInputData() as $key => $singleEntityParams) { - $webapiResolvedParams[$key] = $this->resolveBulkItemParams($singleEntityParams); + $webapiResolvedParams[$key] = $this->resolveBulkItemParams( + $singleEntityParams, + $routeServiceClass, + $routeServiceMethod + ); } + return $webapiResolvedParams; } @@ -115,7 +142,7 @@ public function getInputData() $inputData = $this->request->getRequestData(); $httpMethod = $this->request->getHttpMethod(); - if ($httpMethod == \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE) { + if ($httpMethod == RestRequest::HTTP_METHOD_DELETE) { $requestBodyParams = $this->request->getBodyParams(); $inputData = array_merge($requestBodyParams, $inputData); } @@ -125,7 +152,8 @@ public function getInputData() /** * Returns route. * - * @return \Magento\Webapi\Controller\Rest\Router\Route + * @return Route + * @throws Exception */ public function getRoute() { @@ -142,17 +170,13 @@ public function getRoute() * we don't need to merge body params with url params and use only body params * * @param array $inputData data to send to method in key-value format + * @param string $serviceClass route Service Class + * @param string $serviceMethod route Service Method * @return array list of parameters that can be used to call the service method - * @throws \Magento\Framework\Exception\InputException if no value is provided for required parameters - * @throws \Magento\Framework\Webapi\Exception + * @throws Exception|LocalizedException */ - private function resolveBulkItemParams($inputData) + private function resolveBulkItemParams(array $inputData, string $serviceClass, string $serviceMethod): array { - $route = $this->getRoute(); - $serviceMethodName = $route->getServiceMethod(); - $serviceClassName = $route->getServiceClass(); - $inputParams = $this->serviceInputProcessor->process($serviceClassName, $serviceMethodName, $inputData); - - return $inputParams; + return $this->serviceInputProcessor->process($serviceClass, $serviceMethod, $inputData); } } diff --git a/app/code/Magento/WebapiAsync/Model/ServiceConfig/Converter.php b/app/code/Magento/WebapiAsync/Model/ServiceConfig/Converter.php index 2c85796a3ab19..74a4adb4b1819 100644 --- a/app/code/Magento/WebapiAsync/Model/ServiceConfig/Converter.php +++ b/app/code/Magento/WebapiAsync/Model/ServiceConfig/Converter.php @@ -16,13 +16,17 @@ class Converter implements \Magento\Framework\Config\ConverterInterface /**#@+ * Array keys for config internal representation. */ - const KEY_SERVICES = 'services'; - const KEY_METHOD = 'method'; - const KEY_METHODS = 'methods'; - const KEY_SYNCHRONOUS_INVOCATION_ONLY = 'synchronousInvocationOnly'; - const KEY_ROUTES = 'routes'; + public const KEY_SERVICES = 'services'; + public const KEY_METHOD = 'method'; + public const KEY_METHODS = 'methods'; + public const KEY_SYNCHRONOUS_INVOCATION_ONLY = 'synchronousInvocationOnly'; + public const KEY_ROUTES = 'routes'; + public const KEY_INPUT_ARRAY_SIZE_LIMIT = 'input-array-size-limit'; /**#@-*/ + /** + * @var array + */ private $allowedRouteMethods = [ \Magento\Webapi\Model\Rest\Config::HTTP_METHOD_GET, \Magento\Webapi\Model\Rest\Config::HTTP_METHOD_POST, @@ -32,7 +36,8 @@ class Converter implements \Magento\Framework\Config\ConverterInterface ]; /** - * {@inheritdoc} + * @inheritDoc + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -81,7 +86,10 @@ private function mergeSynchronousInvocationMethodsData( } /** + * Checks if xml node can be converted + * * @param \DOMElement $node + * * @return bool */ private function canConvertXmlNode(\DOMElement $node) @@ -120,7 +128,10 @@ private function initServiceMethodsKey(array &$result, $serviceClass, $serviceMe } /** + * Returns service class + * * @param \DOMElement $service + * * @return null|string */ private function getServiceClass(\DOMElement $service) @@ -131,7 +142,10 @@ private function getServiceClass(\DOMElement $service) } /** + * Returns service method + * * @param \DOMElement $service + * * @return null|string */ private function getServiceMethod(\DOMElement $service) @@ -142,7 +156,10 @@ private function getServiceMethod(\DOMElement $service) } /** + * Checks if synchronous method invocation only + * * @param \DOMElement $serviceNode + * * @return bool */ private function isSynchronousMethodInvocationOnly(\DOMElement $serviceNode) @@ -153,7 +170,10 @@ private function isSynchronousMethodInvocationOnly(\DOMElement $serviceNode) } /** + * Checks if synchronous invocation only true + * * @param \DOMElement $synchronousInvocationOnlyNode + * * @return bool|mixed */ private function isSynchronousInvocationOnlyTrue(\DOMElement $synchronousInvocationOnlyNode = null) @@ -171,7 +191,9 @@ private function isSynchronousInvocationOnlyTrue(\DOMElement $synchronousInvocat /** * Convert and merge "route" nodes, which represent route customizations + * * @param \DOMDocument $source + * * @return array */ private function convertRouteCustomizations($source) @@ -183,18 +205,23 @@ private function convertRouteCustomizations($source) $routeUrl = $this->getRouteUrl($route); $routeMethod = $this->getRouteMethod($route); $routeAlias = $this->getRouteAlias($route); + $inputArraySizeLimit =$this->getInputArraySizeLimit($route); if ($routeUrl && $routeMethod && $routeAlias) { if (!isset($customRoutes[$routeAlias])) { $customRoutes[$routeAlias] = []; } $customRoutes[$routeAlias][$routeMethod] = $routeUrl; + $customRoutes[$routeAlias][self::KEY_INPUT_ARRAY_SIZE_LIMIT] = $inputArraySizeLimit; } } return $customRoutes; } /** + * Returns route url + * * @param \DOMElement $route + * * @return null|string */ private function getRouteUrl($route) @@ -204,7 +231,10 @@ private function getRouteUrl($route) } /** + * Returns route alias + * * @param \DOMElement $route + * * @return null|string */ private function getRouteAlias($route) @@ -214,7 +244,10 @@ private function getRouteAlias($route) } /** + * Returns route method + * * @param \DOMElement $route + * * @return null|string */ private function getRouteMethod($route) @@ -225,11 +258,36 @@ private function getRouteMethod($route) } /** + * Validates method of route + * * @param string $method + * * @return bool */ private function validateRouteMethod($method) { return in_array($method, $this->allowedRouteMethods); } + + /** + * Returns array size limit of input data + * + * @param \DOMElement $routeDOMElement + * @return int|null + */ + private function getInputArraySizeLimit(\DOMElement $routeDOMElement): ?int + { + /** @var \DOMElement $dataDOMElement */ + foreach ($routeDOMElement->getElementsByTagName('data') as $dataDOMElement) { + if ($dataDOMElement->nodeType === XML_ELEMENT_NODE) { + $inputArraySizeLimitDOMNode = $dataDOMElement->attributes + ->getNamedItem(self::KEY_INPUT_ARRAY_SIZE_LIMIT); + return ($inputArraySizeLimitDOMNode instanceof \DOMNode) + ? (int)$inputArraySizeLimitDOMNode->nodeValue + : null; + } + } + + return null; + } } diff --git a/app/code/Magento/WebapiAsync/Plugin/Rest/Config.php b/app/code/Magento/WebapiAsync/Plugin/Rest/Config.php new file mode 100644 index 0000000000000..739f27cbd090c --- /dev/null +++ b/app/code/Magento/WebapiAsync/Plugin/Rest/Config.php @@ -0,0 +1,86 @@ +serviceConfig = $serviceConfig; + } + + /** + * Overrides the rules for an asynchronous request + * + * @param RestConfig $restConfig + * @param array $routes + * @param Request $request + * @return Route[] + * @throws InputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetRestRoutes(RestConfig $restConfig, array $routes, Request $request): array + { + $httpMethod = $request->getHttpMethod(); + if ($httpMethod === Request::HTTP_METHOD_GET || !$this->canProcess($request)) { + return $routes; + } + + $routeConfigs = $this->serviceConfig->getServices()[self::KEY_ROUTES] ?? []; + + /** @var Route $route */ + foreach ($routes as $route) { + $inputArraySizeLimit = null; + foreach ($routeConfigs as $routeConfig) { + if (!isset($routeConfig[$httpMethod]) + || false === strpos($routeConfig[$httpMethod], $route->getRoutePath()) + || !isset($routeConfig[RestConfig::KEY_INPUT_ARRAY_SIZE_LIMIT])) { + continue; + } + $inputArraySizeLimit = $routeConfig[RestConfig::KEY_INPUT_ARRAY_SIZE_LIMIT]; + break; + } + $route->setInputArraySizeLimit($inputArraySizeLimit); + } + + return $routes; + } + + /** + * Allow the process if using the asynchronous Webapi + * + * @param Request $request + * @return bool + */ + private function canProcess(Request $request): bool + { + return preg_match(self::ASYNC_PROCESSOR_PATH, $request->getUri()->getPath()) === 1; + } +} diff --git a/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Converter/webapi_async.php b/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Converter/webapi_async.php index 96cd8073ab563..60a982c58a6cc 100644 --- a/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Converter/webapi_async.php +++ b/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Converter/webapi_async.php @@ -24,8 +24,8 @@ ], ], 'routes' => [ - 'asyncProducts' => ['POST' => 'async/V1/products'], - 'asyncBulkCmsBlocks' => ['POST' => 'async/bulk/V1/cmsBlock'], - 'asyncCustomers' => ['POST' => 'async/V1/customers'] + 'asyncProducts' => ['POST' => 'async/V1/products', 'input-array-size-limit' => 30], + 'asyncBulkCmsBlocks' => ['POST' => 'async/bulk/V1/cmsBlock', 'input-array-size-limit' => null], + 'asyncCustomers' => ['POST' => 'async/V1/customers', 'input-array-size-limit' => null] ] ]; diff --git a/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Converter/webapi_async.xml b/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Converter/webapi_async.xml index be119c7da707d..4f7ae18066aeb 100644 --- a/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Converter/webapi_async.xml +++ b/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Converter/webapi_async.xml @@ -17,7 +17,9 @@ - + + + diff --git a/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Reader/webapi_async.php b/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Reader/webapi_async.php index e41578b6fbdc8..2db3040cc9330 100644 --- a/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Reader/webapi_async.php +++ b/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Reader/webapi_async.php @@ -24,8 +24,8 @@ ], ], 'routes' => [ - 'asyncProducts' => ['POST' => 'async/bulk/V1/products'], - 'asyncBulkCmsPages' => ['POST' => 'async/bulk/V1/cmsPage'], - 'asyncCustomers' => ['POST' => 'async/V1/customers'] + 'asyncProducts' => ['POST' => 'async/bulk/V1/products','input-array-size-limit' => null], + 'asyncBulkCmsPages' => ['POST' => 'async/bulk/V1/cmsPage', 'input-array-size-limit' => 50], + 'asyncCustomers' => ['POST' => 'async/V1/customers', 'input-array-size-limit' => null] ] ]; diff --git a/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Reader/webapi_async_1.xml b/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Reader/webapi_async_1.xml index bd9895c7eef1e..4856c6f0a701e 100644 --- a/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Reader/webapi_async_1.xml +++ b/app/code/Magento/WebapiAsync/Test/Unit/Model/ServiceConfig/_files/Reader/webapi_async_1.xml @@ -16,6 +16,8 @@ - + + + diff --git a/app/code/Magento/WebapiAsync/etc/webapi_async.xsd b/app/code/Magento/WebapiAsync/etc/webapi_async.xsd index c41d9811f2d63..7c175a0f54df7 100644 --- a/app/code/Magento/WebapiAsync/etc/webapi_async.xsd +++ b/app/code/Magento/WebapiAsync/etc/webapi_async.xsd @@ -26,6 +26,9 @@ + + + @@ -40,4 +43,8 @@ + + + + diff --git a/app/code/Magento/WebapiAsync/etc/webapi_rest/di.xml b/app/code/Magento/WebapiAsync/etc/webapi_rest/di.xml index 107754df10db6..e03bea7987396 100644 --- a/app/code/Magento/WebapiAsync/etc/webapi_rest/di.xml +++ b/app/code/Magento/WebapiAsync/etc/webapi_rest/di.xml @@ -17,4 +17,7 @@ - \ No newline at end of file + + + + diff --git a/dev/tests/api-functional/_files/Magento/TestModule1/etc/config.xml b/dev/tests/api-functional/_files/Magento/TestModule1/etc/config.xml new file mode 100644 index 0000000000000..6b1bff7578908 --- /dev/null +++ b/dev/tests/api-functional/_files/Magento/TestModule1/etc/config.xml @@ -0,0 +1,16 @@ + + + + + + + 1 + + + + diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/WebapiAbstract.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/WebapiAbstract.php index 7ccab097d7778..2dd022cc211f5 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/WebapiAbstract.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/WebapiAbstract.php @@ -23,16 +23,16 @@ abstract class WebapiAbstract extends \PHPUnit\Framework\TestCase /**#@+ * Auto tear down options in setFixture */ - const AUTO_TEAR_DOWN_DISABLED = 0; - const AUTO_TEAR_DOWN_AFTER_METHOD = 1; - const AUTO_TEAR_DOWN_AFTER_CLASS = 2; + public const AUTO_TEAR_DOWN_DISABLED = 0; + public const AUTO_TEAR_DOWN_AFTER_METHOD = 1; + public const AUTO_TEAR_DOWN_AFTER_CLASS = 2; /**#@-*/ /**#@+ * Web API adapters that are used to perform actual calls. */ - const ADAPTER_SOAP = 'soap'; - const ADAPTER_REST = 'rest'; + public const ADAPTER_SOAP = 'soap'; + public const ADAPTER_REST = 'rest'; /**#@-*/ /** @@ -566,6 +566,9 @@ protected function _restoreAppConfig() public function processRestExceptionResult(\Exception $e) { $error = json_decode($e->getMessage(), true); + if (json_last_error() !== JSON_ERROR_NONE) { + $error['message'] = $e->getMessage(); + } //Remove line breaks and replace with space $error['message'] = trim(preg_replace('/\s+/', ' ', $error['message'])); // remove trace and type, will only be present if server is in dev mode diff --git a/dev/tests/api-functional/testsuite/Magento/Integration/Model/IntegrationTest.php b/dev/tests/api-functional/testsuite/Magento/Integration/Model/IntegrationTest.php index 489e7d2517527..a5d78c9054936 100644 --- a/dev/tests/api-functional/testsuite/Magento/Integration/Model/IntegrationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Integration/Model/IntegrationTest.php @@ -5,6 +5,8 @@ */ namespace Magento\Integration\Model; +use Magento\Framework\App\ObjectManager; +use Magento\Integration\Api\OauthServiceInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Authentication\OauthHelper; @@ -64,4 +66,45 @@ public function testGetServiceCall() $this->assertEquals($itemId, $item['entity_id'], 'id field returned incorrectly'); $this->assertEquals($name, $item['name'], 'name field returned incorrectly'); } + + /** + * Test Integration access token cannot be used as Bearer token by default + * @magentoConfigFixture default_store oauth/consumer/enable_integration_as_bearer 0 + */ + public function testIntegrationAsBearerTokenDefault() + { + $this->_markTestAsRestOnly(); + $oauthService = ObjectManager::getInstance()->get(OauthServiceInterface::class); + $accessToken = $oauthService->getAccessToken($this->integration->getConsumerId()); + $serviceInfo = [ + 'rest' => [ + 'token' => $accessToken, + 'resourcePath' => '/V1/store/storeViews', + 'httpMethod' => \Magento\Webapi\Model\Rest\Config::HTTP_METHOD_GET, + ], + ]; + self::expectException(\Exception::class); + self::expectExceptionMessage('The consumer isn\'t authorized to access %resources.'); + $this->_webApiCall($serviceInfo); + } + + /** + * Test Integration access token can be used as Bearer token when explicitly enabled + * + * @doesNotPerformAssertions + */ + public function testIntegrationAsBearerTokenEnabled() + { + $this->_markTestAsRestOnly(); + $oauthService = ObjectManager::getInstance()->get(OauthServiceInterface::class); + $accessToken = $oauthService->getAccessToken($this->integration->getConsumerId()); + $serviceInfo = [ + 'rest' => [ + 'token' => $accessToken->getToken(), + 'resourcePath' => '/V1/store/storeViews', + 'httpMethod' => \Magento\Webapi\Model\Rest\Config::HTTP_METHOD_GET, + ], + ]; + $this->_webApiCall($serviceInfo); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/Webapi/WsdlGenerationFromDataObjectTest.php b/dev/tests/api-functional/testsuite/Magento/Webapi/WsdlGenerationFromDataObjectTest.php index 91be257429314..4a05a83351685 100644 --- a/dev/tests/api-functional/testsuite/Magento/Webapi/WsdlGenerationFromDataObjectTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Webapi/WsdlGenerationFromDataObjectTest.php @@ -6,6 +6,8 @@ namespace Magento\Webapi; +use Magento\TestFramework\Authentication\OauthHelper; +use Magento\TestFramework\Authentication\Rest\OauthClient; use Magento\TestFramework\Helper\Bootstrap; /** @@ -33,6 +35,39 @@ protected function setUp(): void parent::setUp(); } + /** + * @magentoConfigFixture default_store oauth/consumer/enable_integration_as_bearer 0 + */ + public function testDisabledIntegrationAsBearer() + { + $wsdlUrl = $this->_getBaseWsdlUrl() . 'testModule5AllSoapAndRestV1,testModule5AllSoapAndRestV2'; + $accessCredentials = \Magento\TestFramework\Authentication\OauthHelper::getApiAccessCredentials()['key']; + $connection = curl_init($wsdlUrl); + curl_setopt($connection, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($connection, CURLOPT_HTTPHEADER, ['header' => "Authorization: Bearer " . $accessCredentials]); + $responseContent = curl_exec($connection); + $this->assertEquals(curl_getinfo($connection, CURLINFO_HTTP_CODE), 401); + $this->assertStringContainsString( + "The consumer isn't authorized to access %resources.", + htmlspecialchars_decode($responseContent, ENT_QUOTES) + ); + } + + public function testAuthenticationWithOAuth() + { + $wsdlUrl = $this->_getBaseWsdlUrl() . 'testModule5AllSoapAndRestV2'; + $this->_soapUrl = "{$this->_baseUrl}/soap/{$this->_storeCode}?services=testModule5AllSoapAndRestV2"; + $this->isSingleService = true; + + $connection = curl_init($wsdlUrl); + curl_setopt($connection, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($connection, CURLOPT_HTTPHEADER, ['header' => $this->getAuthHeader($wsdlUrl)]); + $responseContent = curl_exec($connection); + $this->assertEquals(curl_getinfo($connection, CURLINFO_HTTP_CODE), 200); + $wsdlContent = $this->_convertXmlToString($responseContent); + $this->checkAll($wsdlContent); + } + public function testMultiServiceWsdl() { $this->_soapUrl = "{$this->_baseUrl}/soap/{$this->_storeCode}" @@ -41,12 +76,7 @@ public function testMultiServiceWsdl() $wsdlContent = $this->_convertXmlToString($this->_getWsdlContent($wsdlUrl)); $this->isSingleService = false; - $this->_checkTypesDeclaration($wsdlContent); - $this->_checkPortTypeDeclaration($wsdlContent); - $this->_checkBindingDeclaration($wsdlContent); - $this->_checkServiceDeclaration($wsdlContent); - $this->_checkMessagesDeclaration($wsdlContent); - $this->_checkFaultsDeclaration($wsdlContent); + $this->checkAll($wsdlContent); } public function testSingleServiceWsdl() @@ -56,12 +86,7 @@ public function testSingleServiceWsdl() $wsdlContent = $this->_convertXmlToString($this->_getWsdlContent($wsdlUrl)); $this->isSingleService = true; - $this->_checkTypesDeclaration($wsdlContent); - $this->_checkPortTypeDeclaration($wsdlContent); - $this->_checkBindingDeclaration($wsdlContent); - $this->_checkServiceDeclaration($wsdlContent); - $this->_checkMessagesDeclaration($wsdlContent); - $this->_checkFaultsDeclaration($wsdlContent); + $this->checkAll($wsdlContent); } public function testNoAuthorizedServices() @@ -983,4 +1008,28 @@ protected function _checkFaultsComplexTypeSection($wsdlContent) 'Details wrapped errors (array of wrapped errors) complex types declaration is invalid.' ); } + + private function getAuthHeader(string $url): string + { + $accessCredentials = OauthHelper::getApiAccessCredentials(); + /** @var OauthClient $oAuthClient */ + $oAuthClient = $accessCredentials['oauth_client']; + return $oAuthClient->buildOauthAuthorizationHeader( + $url, + $accessCredentials['key'], + $accessCredentials['secret'], + [], + 'GET' + )[0]; + } + + private function checkAll(string $wsdlContent): void + { + $this->_checkTypesDeclaration($wsdlContent); + $this->_checkPortTypeDeclaration($wsdlContent); + $this->_checkBindingDeclaration($wsdlContent); + $this->_checkServiceDeclaration($wsdlContent); + $this->_checkMessagesDeclaration($wsdlContent); + $this->_checkFaultsDeclaration($wsdlContent); + } } diff --git a/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapi.php b/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapi.php index 57c8fbf45c63c..7d2be5f5a82d2 100644 --- a/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapi.php +++ b/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapi.php @@ -13,7 +13,8 @@ ], 'secure' => false, 'realMethod' => 'item', - 'parameters' => [] + 'parameters' => [], + 'input-array-size-limit' => null, ], 'create' => [ 'resources' => [ @@ -21,7 +22,8 @@ ], 'secure' => false, 'realMethod' => 'create', - 'parameters' => [] + 'parameters' => [], + 'input-array-size-limit' => null, ], ], ], @@ -34,7 +36,8 @@ ], 'secure' => false, 'realMethod' => 'getPreconfiguredItem', - 'parameters' => [] + 'parameters' => [], + 'input-array-size-limit' => null, ], ], ], @@ -47,7 +50,8 @@ ], 'secure' => false, 'realMethod' => 'item', - 'parameters' => [] + 'parameters' => [], + 'input-array-size-limit' => null, ], 'itemDefault' => [ 'resources' => [ @@ -60,7 +64,8 @@ 'force' => true, 'value' => null, ], - ] + ], + 'input-array-size-limit' => null, ], 'create' => [ 'resources' => [ @@ -73,7 +78,8 @@ 'force' => true, 'value' => null, ], - ] + ], + 'input-array-size-limit' => null, ], ], ], @@ -87,7 +93,8 @@ ], 'secure' => false, 'realMethod' => 'item', - 'parameters' => [] + 'parameters' => [], + 'input-array-size-limit' => null, ], 'create' => [ 'resources' => [ @@ -101,7 +108,8 @@ 'force' => true, 'value' => null, ], - ] + ], + 'input-array-size-limit' => 50, ], 'delete' => [ 'resources' => [ @@ -110,7 +118,8 @@ ], 'secure' => false, 'realMethod' => 'delete', - 'parameters' => [] + 'parameters' => [], + 'input-array-size-limit' => null, ], 'update' => [ 'resources' => [ @@ -119,7 +128,8 @@ ], 'secure' => false, 'realMethod' => 'update', - 'parameters' => [] + 'parameters' => [], + 'input-array-size-limit' => null, ], ], ], @@ -136,8 +146,8 @@ 'resources' => [ 'Magento_TestModuleMSC::resource1' => true, ], - 'parameters' => [ - ], + 'parameters' => [], + 'input-array-size-limit' => null, ], ], '/V1/testmoduleMSC' => [ @@ -150,8 +160,8 @@ 'resources' => [ 'Magento_TestModuleMSC::resource3' => true, ], - 'parameters' => [ - ], + 'parameters' => [], + 'input-array-size-limit' => null, ], ], '/V1/testmodule1/:id' => [ @@ -164,8 +174,8 @@ 'resources' => [ 'Magento_Test1::resource1' => true, ], - 'parameters' => [ - ], + 'parameters' => [], + 'input-array-size-limit' => null, ], ], '/V1/testmodule1' => [ @@ -184,6 +194,7 @@ 'value' => null, ], ], + 'input-array-size-limit' => null, ], 'POST' => [ 'secure' => false, @@ -200,6 +211,7 @@ 'value' => null, ], ], + 'input-array-size-limit' => null, ], ], '/V2/testmodule1/:id' => [ @@ -213,8 +225,8 @@ 'Magento_Test1::resource1' => true, 'Magento_Test1::resource2' => true, ], - 'parameters' => [ - ], + 'parameters' => [], + 'input-array-size-limit' => null, ], 'DELETE' => [ 'secure' => false, @@ -226,8 +238,8 @@ 'Magento_Test1::resource1' => true, 'Magento_Test1::resource2' => true, ], - 'parameters' => [ - ], + 'parameters' => [], + 'input-array-size-limit' => null, ], 'PUT' => [ 'secure' => false, @@ -239,8 +251,8 @@ 'Magento_Test1::resource1' => true, 'Magento_Test1::resource2' => true, ], - 'parameters' => [ - ], + 'parameters' => [], + 'input-array-size-limit' => null, ], ], '/V2/testmodule1' => [ @@ -260,6 +272,7 @@ 'value' => null, ], ], + 'input-array-size-limit' => 50, ], ], '/V2/testmoduleMSC/itemPreconfigured' => [ @@ -274,6 +287,7 @@ 'Magento_TestModuleMSC::resource2' => true, ], 'parameters' => [], + 'input-array-size-limit' => null, ] ] ], diff --git a/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapiA.xml b/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapiA.xml index 389908309b2a2..c84aedf09cf1a 100644 --- a/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapiA.xml +++ b/dev/tests/integration/testsuite/Magento/Webapi/Model/Config/_files/webapiA.xml @@ -35,7 +35,7 @@ - + null diff --git a/dev/tests/integration/testsuite/Magento/Webapi/Model/Soap/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Webapi/Model/Soap/ConfigTest.php index ceca6403b2c96..e0457df1fac19 100644 --- a/dev/tests/integration/testsuite/Magento/Webapi/Model/Soap/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Webapi/Model/Soap/ConfigTest.php @@ -90,7 +90,8 @@ public function testGetServiceMethodInfo() 'resources' => [ 'Magento_Customer::customer', ], - 'parameters' => [] + 'parameters' => [], + 'input-array-size-limit' => null, ]; $actual = $this->soapConfig->getServiceMethodInfo( 'customerCustomerRepositoryV1GetById', diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Webapi/Model/ConfigTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Webapi/Model/ConfigTest.php index c6d90fd55af93..359b5bda7cf6c 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Webapi/Model/ConfigTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Magento/Webapi/Model/ConfigTest.php @@ -22,7 +22,6 @@ public function testSchemaUsingInvalidXml($expectedErrors = null) "Element 'route', attribute 'method': [facet 'enumeration'] The value 'PATCH' is not an element of the set {'GET', 'PUT', 'POST', 'DELETE'}.", "Element 'route', attribute 'method': 'PATCH' is not a valid value of the local atomic type.", "Element 'service': The attribute 'method' is required but missing.", - "Element 'data': Missing child element(s). Expected is ( parameter ).", "Element 'route': Missing child element(s). Expected is ( service ).", "Element 'route': Missing child element(s). Expected is ( resources ).", ]; @@ -40,7 +39,6 @@ public function testFileSchemaUsingInvalidXml($expectedErrors = null) "Element 'route', attribute 'method': [facet 'enumeration'] The value 'PATCH' is not an element of the set {'GET', 'PUT', 'POST', 'DELETE'}.", "Element 'route', attribute 'method': 'PATCH' is not a valid value of the local atomic type.", "Element 'service': The attribute 'method' is required but missing.", - "Element 'data': Missing child element(s). Expected is ( parameter ).", ]; // @codingStandardsIgnoreEnd parent::testFileSchemaUsingInvalidXml($expectedErrors); diff --git a/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php b/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php index 96999187bd6cb..670c74dd197bc 100644 --- a/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php +++ b/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php @@ -18,143 +18,153 @@ class ConfigOptionsListConstants /**#@+ * Path to the values in the deployment config */ - const CONFIG_PATH_INSTALL_DATE = 'install/date'; - const CONFIG_PATH_CRYPT_KEY = 'crypt/key'; - const CONFIG_PATH_SESSION_SAVE = 'session/save'; - const CONFIG_PATH_RESOURCE_DEFAULT_SETUP = 'resource/default_setup/connection'; - const CONFIG_PATH_DB_CONNECTION_DEFAULT_DRIVER_OPTIONS = 'db/connection/default/driver_options'; - const CONFIG_PATH_DB_CONNECTION_DEFAULT = 'db/connection/default'; - const CONFIG_PATH_DB_CONNECTIONS = 'db/connection'; - const CONFIG_PATH_DB_PREFIX = 'db/table_prefix'; - const CONFIG_PATH_X_FRAME_OPT = 'x-frame-options'; - const CONFIG_PATH_CACHE_HOSTS = 'http_cache_hosts'; - const CONFIG_PATH_BACKEND = 'backend'; - const CONFIG_PATH_INSTALL = 'install'; - const CONFIG_PATH_CRYPT = 'crypt'; - const CONFIG_PATH_SESSION = 'session'; - const CONFIG_PATH_DB = 'db'; - const CONFIG_PATH_RESOURCE = 'resource'; - const CONFIG_PATH_CACHE_TYPES = 'cache_types'; - const CONFIG_PATH_DOCUMENT_ROOT_IS_PUB = 'directories/document_root_is_pub'; - const CONFIG_PATH_DB_LOGGER_OUTPUT = 'db_logger/output'; - const CONFIG_PATH_DB_LOGGER_LOG_EVERYTHING = 'db_logger/log_everything'; - const CONFIG_PATH_DB_LOGGER_QUERY_TIME_THRESHOLD = 'db_logger/query_time_threshold'; - const CONFIG_PATH_DB_LOGGER_INCLUDE_STACKTRACE = 'db_logger/include_stacktrace'; + public const CONFIG_PATH_INSTALL_DATE = 'install/date'; + public const CONFIG_PATH_CRYPT_KEY = 'crypt/key'; + public const CONFIG_PATH_SESSION_SAVE = 'session/save'; + public const CONFIG_PATH_RESOURCE_DEFAULT_SETUP = 'resource/default_setup/connection'; + public const CONFIG_PATH_DB_CONNECTION_DEFAULT_DRIVER_OPTIONS = 'db/connection/default/driver_options'; + public const CONFIG_PATH_DB_CONNECTION_DEFAULT = 'db/connection/default'; + public const CONFIG_PATH_DB_CONNECTIONS = 'db/connection'; + public const CONFIG_PATH_DB_PREFIX = 'db/table_prefix'; + public const CONFIG_PATH_X_FRAME_OPT = 'x-frame-options'; + public const CONFIG_PATH_CACHE_HOSTS = 'http_cache_hosts'; + public const CONFIG_PATH_BACKEND = 'backend'; + public const CONFIG_PATH_INSTALL = 'install'; + public const CONFIG_PATH_CRYPT = 'crypt'; + public const CONFIG_PATH_SESSION = 'session'; + public const CONFIG_PATH_DB = 'db'; + public const CONFIG_PATH_RESOURCE = 'resource'; + public const CONFIG_PATH_CACHE_TYPES = 'cache_types'; + public const CONFIG_PATH_DOCUMENT_ROOT_IS_PUB = 'directories/document_root_is_pub'; + public const CONFIG_PATH_DB_LOGGER_OUTPUT = 'db_logger/output'; + public const CONFIG_PATH_DB_LOGGER_LOG_EVERYTHING = 'db_logger/log_everything'; + public const CONFIG_PATH_DB_LOGGER_QUERY_TIME_THRESHOLD = 'db_logger/query_time_threshold'; + public const CONFIG_PATH_DB_LOGGER_INCLUDE_STACKTRACE = 'db_logger/include_stacktrace'; /**#@-*/ /** * Parameter for disabling/enabling static content deployment on demand in production mode * Can contains 0/1 value */ - const CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION = 'static_content_on_demand_in_production'; + public const CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION = 'static_content_on_demand_in_production'; /** * Parameter for forcing HTML minification even if file is already minified. */ - const CONFIG_PATH_FORCE_HTML_MINIFICATION = 'force_html_minification'; + public const CONFIG_PATH_FORCE_HTML_MINIFICATION = 'force_html_minification'; + + /** + * Default limiting input array size for synchronous Web API + */ + public const CONFIG_PATH_WEBAPI_SYNC_DEFAULT_INPUT_ARRAY_SIZE_LIMIT = 'webapi/sync/default_input_array_size_limit'; + + /** + * Default limiting input array size for asynchronous Web API + * phpcs:disable + */ + public const CONFIG_PATH_WEBAPI_ASYNC_DEFAULT_INPUT_ARRAY_SIZE_LIMIT = 'webapi/async/default_input_array_size_limit'; + //phpcs:enable /**#@+ * Input keys for the options */ - const INPUT_KEY_ENCRYPTION_KEY = 'key'; - const INPUT_KEY_SESSION_SAVE = 'session-save'; - const INPUT_KEY_DB_HOST = 'db-host'; - const INPUT_KEY_DB_NAME = 'db-name'; - const INPUT_KEY_DB_USER = 'db-user'; - const INPUT_KEY_DB_PASSWORD = 'db-password'; - const INPUT_KEY_DB_PREFIX = 'db-prefix'; - const INPUT_KEY_DB_MODEL = 'db-model'; - const INPUT_KEY_DB_INIT_STATEMENTS = 'db-init-statements'; - const INPUT_KEY_DB_ENGINE = 'db-engine'; - const INPUT_KEY_DB_SSL_KEY = 'db-ssl-key'; - const INPUT_KEY_DB_SSL_CERT = 'db-ssl-cert'; - const INPUT_KEY_DB_SSL_CA = 'db-ssl-ca'; - const INPUT_KEY_DB_SSL_VERIFY = 'db-ssl-verify'; - const INPUT_KEY_RESOURCE = 'resource'; - const INPUT_KEY_SKIP_DB_VALIDATION = 'skip-db-validation'; - const INPUT_KEY_CACHE_HOSTS = 'http-cache-hosts'; + public const INPUT_KEY_ENCRYPTION_KEY = 'key'; + public const INPUT_KEY_SESSION_SAVE = 'session-save'; + public const INPUT_KEY_DB_HOST = 'db-host'; + public const INPUT_KEY_DB_NAME = 'db-name'; + public const INPUT_KEY_DB_USER = 'db-user'; + public const INPUT_KEY_DB_PASSWORD = 'db-password'; + public const INPUT_KEY_DB_PREFIX = 'db-prefix'; + public const INPUT_KEY_DB_MODEL = 'db-model'; + public const INPUT_KEY_DB_INIT_STATEMENTS = 'db-init-statements'; + public const INPUT_KEY_DB_ENGINE = 'db-engine'; + public const INPUT_KEY_DB_SSL_KEY = 'db-ssl-key'; + public const INPUT_KEY_DB_SSL_CERT = 'db-ssl-cert'; + public const INPUT_KEY_DB_SSL_CA = 'db-ssl-ca'; + public const INPUT_KEY_DB_SSL_VERIFY = 'db-ssl-verify'; + public const INPUT_KEY_RESOURCE = 'resource'; + public const INPUT_KEY_SKIP_DB_VALIDATION = 'skip-db-validation'; + public const INPUT_KEY_CACHE_HOSTS = 'http-cache-hosts'; /**#@-*/ /**#@+ * Input keys for cache configuration */ - const KEY_CACHE_FRONTEND = 'cache/frontend'; - const CONFIG_PATH_BACKEND_OPTIONS = 'backend_options'; + public const KEY_CACHE_FRONTEND = 'cache/frontend'; + public const CONFIG_PATH_BACKEND_OPTIONS = 'backend_options'; /** - * @deprecated - * * Definition format constant. */ - const INPUT_KEY_DEFINITION_FORMAT = 'definition-format'; + public const INPUT_KEY_DEFINITION_FORMAT = 'definition-format'; /**#@+ * Values for session-save */ - const SESSION_SAVE_FILES = 'files'; - const SESSION_SAVE_DB = 'db'; - const SESSION_SAVE_REDIS = 'redis'; + public const SESSION_SAVE_FILES = 'files'; + public const SESSION_SAVE_DB = 'db'; + public const SESSION_SAVE_REDIS = 'redis'; /**#@-*/ /** * Array Key for session save method */ - const KEY_SAVE = 'save'; + public const KEY_SAVE = 'save'; /**#@+ * Array keys for Database configuration */ - const KEY_HOST = 'host'; - const KEY_PORT = 'port'; - const KEY_NAME = 'dbname'; - const KEY_USER = 'username'; - const KEY_PASSWORD = 'password'; - const KEY_ENGINE = 'engine'; - const KEY_PREFIX = 'table_prefix'; - const KEY_MODEL = 'model'; - const KEY_INIT_STATEMENTS = 'initStatements'; - const KEY_ACTIVE = 'active'; - const KEY_DRIVER_OPTIONS = 'driver_options'; + public const KEY_HOST = 'host'; + public const KEY_PORT = 'port'; + public const KEY_NAME = 'dbname'; + public const KEY_USER = 'username'; + public const KEY_PASSWORD = 'password'; + public const KEY_ENGINE = 'engine'; + public const KEY_PREFIX = 'table_prefix'; + public const KEY_MODEL = 'model'; + public const KEY_INIT_STATEMENTS = 'initStatements'; + public const KEY_ACTIVE = 'active'; + public const KEY_DRIVER_OPTIONS = 'driver_options'; /**#@-*/ /**#@+ * Array keys for database driver options configurations */ - const KEY_MYSQL_SSL_KEY = \PDO::MYSQL_ATTR_SSL_KEY; - const KEY_MYSQL_SSL_CERT = \PDO::MYSQL_ATTR_SSL_CERT; - const KEY_MYSQL_SSL_CA = \PDO::MYSQL_ATTR_SSL_CA; + public const KEY_MYSQL_SSL_KEY = \PDO::MYSQL_ATTR_SSL_KEY; + public const KEY_MYSQL_SSL_CERT = \PDO::MYSQL_ATTR_SSL_CERT; + public const KEY_MYSQL_SSL_CA = \PDO::MYSQL_ATTR_SSL_CA; /** * Constant \PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT cannot be used as it was introduced in PHP 7.1.4 * and Magento 2 is currently supporting PHP 7.1.3. */ - const KEY_MYSQL_SSL_VERIFY = 1014; + public const KEY_MYSQL_SSL_VERIFY = 1014; /**#@-*/ /** * Db config key */ - const KEY_DB = 'db'; + public const KEY_DB = 'db'; /** * Array Key for encryption key in deployment config file */ - const KEY_ENCRYPTION_KEY = 'key'; + public const KEY_ENCRYPTION_KEY = 'key'; /** * Resource config key */ - const KEY_RESOURCE = 'resource'; + public const KEY_RESOURCE = 'resource'; /** * Key for modules */ - const KEY_MODULES = 'modules'; + public const KEY_MODULES = 'modules'; /** * Size of random string generated for store's encryption key * phpcs:disable */ - const STORE_KEY_RANDOM_STRING_SIZE = SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES; + public const STORE_KEY_RANDOM_STRING_SIZE = SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES; //phpcs:enable } diff --git a/lib/internal/Magento/Framework/Webapi/Test/Unit/ServiceInputProcessorTest.php b/lib/internal/Magento/Framework/Webapi/Test/Unit/ServiceInputProcessorTest.php index 6aef1a61922c8..ff1c538402133 100644 --- a/lib/internal/Magento/Framework/Webapi/Test/Unit/ServiceInputProcessorTest.php +++ b/lib/internal/Magento/Framework/Webapi/Test/Unit/ServiceInputProcessorTest.php @@ -39,6 +39,7 @@ use Magento\Webapi\Test\Unit\Service\Entity\SimpleData; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Webapi\Validator\EntityArrayValidator\InputArraySizeLimitValue; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -167,6 +168,8 @@ function () use ($objectManager) { ->disableOriginalConstructor() ->getMock(); + $inputArraySizeLimitValue = $this->createMock(InputArraySizeLimitValue::class); + $this->defaultPageSizeSetter = self::getMockBuilder(DefaultPageSizeSetter::class) ->disableOriginalConstructor() ->getMock(); @@ -180,7 +183,11 @@ function () use ($objectManager) { 'attributeValueFactory' => $this->attributeValueFactoryMock, 'methodsMap' => $this->methodsMap, 'serviceTypeToEntityTypeMap' => $this->serviceTypeToEntityTypeMap, - 'serviceInputValidator' => new EntityArrayValidator(50, $this->inputLimitConfig), + 'serviceInputValidator' => new EntityArrayValidator( + 50, + $this->inputLimitConfig, + $inputArraySizeLimitValue + ), 'defaultPageSizeSetter' => $this->defaultPageSizeSetter, 'defaultPageSize' => 123 ] diff --git a/lib/internal/Magento/Framework/Webapi/Test/Unit/Validator/EntityArrayValidator/InputArraySizeLimitValueTest.php b/lib/internal/Magento/Framework/Webapi/Test/Unit/Validator/EntityArrayValidator/InputArraySizeLimitValueTest.php new file mode 100644 index 0000000000000..8b3d089225b06 --- /dev/null +++ b/lib/internal/Magento/Framework/Webapi/Test/Unit/Validator/EntityArrayValidator/InputArraySizeLimitValueTest.php @@ -0,0 +1,95 @@ +requestMock = $this->createMock(Request::class); + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->inputArraySizeLimitValue = new InputArraySizeLimitValue( + $this->requestMock, + $this->deploymentConfigMock + ); + } + + /** + * @throws FileSystemException + * @throws RuntimeException + */ + public function testIfValueNotNull() + { + $this->requestMock->expects(self::never()) + ->method('getPathInfo'); + $this->deploymentConfigMock->expects(self::never()) + ->method('get'); + $this->inputArraySizeLimitValue->set(3); + $this->assertEquals(3, $this->inputArraySizeLimitValue->get()); + } + + /** + * @throws FileSystemException + * @throws RuntimeException + */ + public function testIfValueNullAndRequestIsAsync() + { + $this->requestMock->expects(self::once()) + ->method('getPathInfo') + ->willReturn('/async/V1/path'); + $this->deploymentConfigMock->expects(self::once()) + ->method('get') + ->willReturn(40); + $this->assertEquals(40, $this->inputArraySizeLimitValue->get()); + } + + /** + * @throws FileSystemException + * @throws RuntimeException + */ + public function testIfValueNullAndRequestIsSync() + { + $this->requestMock->expects(self::once()) + ->method('getPathInfo') + ->willReturn('/V1/path'); + $this->deploymentConfigMock->expects(self::once()) + ->method('get') + ->willReturn(50); + $this->assertEquals(50, $this->inputArraySizeLimitValue->get()); + } +} diff --git a/lib/internal/Magento/Framework/Webapi/Test/Unit/Validator/EntityArrayValidatorTest.php b/lib/internal/Magento/Framework/Webapi/Test/Unit/Validator/EntityArrayValidatorTest.php index 6185e80cd342f..91ac018d40cbe 100644 --- a/lib/internal/Magento/Framework/Webapi/Test/Unit/Validator/EntityArrayValidatorTest.php +++ b/lib/internal/Magento/Framework/Webapi/Test/Unit/Validator/EntityArrayValidatorTest.php @@ -11,6 +11,7 @@ use Magento\Framework\Exception\InvalidArgumentException; use Magento\Framework\Webapi\Validator\IOLimit\IOLimitConfigProvider; use Magento\Framework\Webapi\Validator\EntityArrayValidator; +use Magento\Framework\Webapi\Validator\EntityArrayValidator\InputArraySizeLimitValue; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -22,19 +23,64 @@ class EntityArrayValidatorTest extends TestCase /** * @var IOLimitConfigProvider|MockObject */ - private $config; + private $configMock; + + /** + * @var InputArraySizeLimitValue|MockObject + */ + private $inputArraySizeLimitValueMock; /** * @var EntityArrayValidator */ - private $validator; + private EntityArrayValidator $validator; + /** + * @inheritDoc + */ protected function setUp(): void { - $this->config = self::getMockBuilder(IOLimitConfigProvider::class) - ->disableOriginalConstructor() - ->getMock(); - $this->validator = new EntityArrayValidator(3, $this->config); + $this->configMock = $this->createMock(IOLimitConfigProvider::class); + $this->inputArraySizeLimitValueMock = $this->createMock(InputArraySizeLimitValue::class); + $this->validator = new EntityArrayValidator( + 3, + $this->configMock, + $this->inputArraySizeLimitValueMock + ); + } + + /** + * @doesNotPerformAssertions + */ + public function testAllowsDataWhenBelowLimitWhenUsingRouteInputLimit() + { + $this->configMock->expects(self::once()) + ->method('isInputLimitingEnabled') + ->willReturn(true); + $this->inputArraySizeLimitValueMock->expects(self::once()) + ->method('get') + ->willReturn(5); + $this->configMock->expects(self::never()) + ->method('getComplexArrayItemLimit'); + $this->validator->validateComplexArrayType("foo", array_fill(0, 5, [])); + } + + /** + * @doesNotPerformAssertions + */ + public function testFailsDataWhenAboveLimitUsingRouteInputLimit() + { + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessage('Maximum items of type "foo" is 4'); + $this->configMock->expects(self::once()) + ->method('isInputLimitingEnabled') + ->willReturn(true); + $this->inputArraySizeLimitValueMock->expects(self::once()) + ->method('get') + ->willReturn(4); + $this->configMock->expects(self::never()) + ->method('getComplexArrayItemLimit'); + $this->validator->validateComplexArrayType("foo", array_fill(0, 5, [])); } /** @@ -42,9 +88,16 @@ protected function setUp(): void */ public function testAllowsDataWhenBelowLimit() { - $this->config->method('isInputLimitingEnabled') + $this->configMock->expects(self::once()) + ->method('isInputLimitingEnabled') ->willReturn(true); - $this->validator->validateComplexArrayType("foo", [[],[],[]]); + $this->inputArraySizeLimitValueMock->expects(self::once()) + ->method('get') + ->willReturn(null); + $this->configMock->expects(self::once()) + ->method('getComplexArrayItemLimit') + ->willReturn(null); + $this->validator->validateComplexArrayType("foo", array_fill(0, 3, [])); } /** @@ -52,31 +105,54 @@ public function testAllowsDataWhenBelowLimit() */ public function testAllowsDataWhenBelowLimitUsingConfig() { - $this->config->method('isInputLimitingEnabled') + $this->configMock->expects(self::once()) + ->method('isInputLimitingEnabled') ->willReturn(true); - $this->config->method('getComplexArrayItemLimit') + $this->inputArraySizeLimitValueMock->expects(self::once()) + ->method('get') + ->willReturn(null); + $this->configMock->expects(self::once()) + ->method('getComplexArrayItemLimit') ->willReturn(6); - $this->validator->validateComplexArrayType("foo", [[],[],[],[],[]]); + $this->validator->validateComplexArrayType("foo", array_fill(0, 5, [])); } + /** + * @doesNotPerformAssertions + */ public function testFailsDataWhenAboveLimit() { $this->expectException(InvalidArgumentException::class); $this->expectErrorMessage('Maximum items of type "foo" is 3'); - $this->config->method('isInputLimitingEnabled') + $this->configMock->expects(self::once()) + ->method('isInputLimitingEnabled') ->willReturn(true); - $this->validator->validateComplexArrayType("foo", [[],[],[],[]]); + $this->inputArraySizeLimitValueMock->expects(self::once()) + ->method('get') + ->willReturn(null); + $this->configMock->expects(self::once()) + ->method('getComplexArrayItemLimit') + ->willReturn(null); + $this->validator->validateComplexArrayType("foo", array_fill(0, 4, [])); } + /** + * @doesNotPerformAssertions + */ public function testFailsDataWhenAboveLimitUsingConfig() { $this->expectException(InvalidArgumentException::class); $this->expectErrorMessage('Maximum items of type "foo" is 6'); - $this->config->method('isInputLimitingEnabled') + $this->configMock->expects(self::once()) + ->method('isInputLimitingEnabled') ->willReturn(true); - $this->config->method('getComplexArrayItemLimit') + $this->inputArraySizeLimitValueMock->expects(self::once()) + ->method('get') + ->willReturn(null); + $this->configMock->expects(self::once()) + ->method('getComplexArrayItemLimit') ->willReturn(6); - $this->validator->validateComplexArrayType("foo", [[],[],[],[],[],[],[]]); + $this->validator->validateComplexArrayType("foo", array_fill(0, 7, [])); } /** @@ -84,8 +160,11 @@ public function testFailsDataWhenAboveLimitUsingConfig() */ public function testAboveLimitWithDisabledLimiting() { - $this->config->method('isInputLimitingEnabled') + $this->configMock->expects(self::once()) + ->method('isInputLimitingEnabled') ->willReturn(false); - $this->validator->validateComplexArrayType("foo", [[],[],[],[],[],[],[]]); + $this->configMock->expects(self::never()) + ->method('getComplexArrayItemLimit'); + $this->validator->validateComplexArrayType("foo", array_fill(0, 7, [])); } } diff --git a/lib/internal/Magento/Framework/Webapi/Validator/EntityArrayValidator.php b/lib/internal/Magento/Framework/Webapi/Validator/EntityArrayValidator.php index 91019006af301..65a3df9bd7696 100644 --- a/lib/internal/Magento/Framework/Webapi/Validator/EntityArrayValidator.php +++ b/lib/internal/Magento/Framework/Webapi/Validator/EntityArrayValidator.php @@ -9,8 +9,11 @@ namespace Magento\Framework\Webapi\Validator; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\InvalidArgumentException; use Magento\Framework\Webapi\Validator\IOLimit\IOLimitConfigProvider; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\Webapi\Validator\EntityArrayValidator\InputArraySizeLimitValue; /** * Validates service input @@ -20,26 +23,38 @@ class EntityArrayValidator implements ServiceInputValidatorInterface /** * @var int */ - private $complexArrayItemLimit; + private int $complexArrayItemLimit; /** - * @var IOLimitConfigProvider|null + * @var IOLimitConfigProvider */ private $configProvider; + /** + * @var InputArraySizeLimitValue + */ + private $inputArraySizeLimitValue; + /** * @param int $complexArrayItemLimit * @param IOLimitConfigProvider|null $configProvider + * @param InputArraySizeLimitValue|null $inputArraySizeLimitValue */ - public function __construct(int $complexArrayItemLimit, ?IOLimitConfigProvider $configProvider = null) - { + public function __construct( + int $complexArrayItemLimit, + ?IOLimitConfigProvider $configProvider = null, + ?InputArraySizeLimitValue $inputArraySizeLimitValue = null + ) { $this->complexArrayItemLimit = $complexArrayItemLimit; - $this->configProvider = $configProvider ?? ObjectManager::getInstance() - ->get(IOLimitConfigProvider::class); + $this->configProvider = $configProvider ?? ObjectManager::getInstance()->get(IOLimitConfigProvider::class); + $this->inputArraySizeLimitValue = $inputArraySizeLimitValue ?? ObjectManager::getInstance() + ->get(InputArraySizeLimitValue::class); } /** * @inheritDoc + * + * @throws FileSystemException|RuntimeException */ public function validateComplexArrayType(string $className, array $items): void { @@ -47,13 +62,14 @@ public function validateComplexArrayType(string $className, array $items): void return; } - $max = $this->configProvider->getComplexArrayItemLimit() ?? $this->complexArrayItemLimit; + $maxLimit = $this->inputArraySizeLimitValue->get() + ?? ($this->configProvider->getComplexArrayItemLimit() ?? $this->complexArrayItemLimit); - if (count($items) > $max) { + if (count($items) > $maxLimit) { throw new InvalidArgumentException( __( 'Maximum items of type "%type" is %max', - ['type' => $className, 'max' => $max] + ['type' => $className, 'max' => $maxLimit] ) ); } diff --git a/lib/internal/Magento/Framework/Webapi/Validator/EntityArrayValidator/InputArraySizeLimitValue.php b/lib/internal/Magento/Framework/Webapi/Validator/EntityArrayValidator/InputArraySizeLimitValue.php new file mode 100644 index 0000000000000..ca75da4a93eed --- /dev/null +++ b/lib/internal/Magento/Framework/Webapi/Validator/EntityArrayValidator/InputArraySizeLimitValue.php @@ -0,0 +1,95 @@ +request = $request; + $this->deploymentConfig = $deploymentConfig; + } + + /** + * Set value of input array size limit + * + * @param int|null $value + */ + public function set(?int $value): void + { + $this->value = $value; + } + + /** + * Get value of input array size limit + * + * @return int|null + * @throws FileSystemException + * @throws RuntimeException + */ + public function get(): ?int + { + return $this->value ?? ($this->isAsync() + ? $this->deploymentConfig->get( + ConstantList::CONFIG_PATH_WEBAPI_ASYNC_DEFAULT_INPUT_ARRAY_SIZE_LIMIT, + self::DEFAULT_ASYNC_INPUT_ARRAY_SIZE_LIMIT + ) + : $this->deploymentConfig->get( + ConstantList::CONFIG_PATH_WEBAPI_SYNC_DEFAULT_INPUT_ARRAY_SIZE_LIMIT + ) + ); + } + + /** + * Returns true if using asynchronous Webapi + * + * @return bool + */ + private function isAsync(): bool + { + return preg_match(self::ASYNC_PROCESSOR_PATH, $this->request->getPathInfo()) === 1; + } +}