diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php new file mode 100644 index 0000000000000..ba2e995d4f704 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php @@ -0,0 +1,71 @@ +corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Get value for header + * + * @return string + */ + public function getValue(): string + { + return "1"; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->corsConfiguration->isCredentialsAllowed(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php new file mode 100644 index 0000000000000..68760de543daa --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php @@ -0,0 +1,71 @@ +corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedHeaders(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php new file mode 100644 index 0000000000000..233839b9deb74 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php @@ -0,0 +1,71 @@ +corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedMethods(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php new file mode 100644 index 0000000000000..21850f18db1f2 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php @@ -0,0 +1,71 @@ +corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedOrigins(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php new file mode 100644 index 0000000000000..e30209ae25e68 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php @@ -0,0 +1,71 @@ +corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return (string) $this->corsConfiguration->getMaxAge(); + } +} diff --git a/app/code/Magento/GraphQl/Model/Cors/Configuration.php b/app/code/Magento/GraphQl/Model/Cors/Configuration.php new file mode 100644 index 0000000000000..dd5a0b426e22d --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Cors/Configuration.php @@ -0,0 +1,96 @@ +scopeConfig = $scopeConfig; + } + + /** + * Are CORS headers enabled + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_HEADERS_ENABLED); + } + + /** + * Get allowed origins or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedOrigins(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_ORIGINS); + } + + /** + * Get allowed headers or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedHeaders(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_HEADERS); + } + + /** + * Get allowed methods or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedMethods(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_METHODS); + } + + /** + * Get max age header value + * + * @return int + */ + public function getMaxAge(): int + { + return (int) $this->scopeConfig->getValue(self::XML_PATH_CORS_MAX_AGE); + } + + /** + * Are credentials allowed + * + * @return bool + */ + public function isCredentialsAllowed(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_ALLOW_CREDENTIALS); + } +} diff --git a/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php new file mode 100644 index 0000000000000..b40b64f48e51f --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php @@ -0,0 +1,56 @@ + + + + +
+ + service + Magento_Integration::config_oauth + + + + + Magento\Config\Model\Config\Source\Yesno + + + + + The Access-Control-Allow-Origin response header indicates whether the response can be shared with requesting code from the given origin. Fill this field with one or more origins (comma separated) or use '*' to allow access from all origins. + + 1 + + + + + + The Access-Control-Allow-Methods response header specifies the method or methods allowed when accessing the resource in response to a preflight request. Use comma separated methods (e.g. GET,POST) + + 1 + + + + + + The Access-Control-Allow-Headers response header is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request. Use comma separated headers. + + 1 + + + + + + validate-digits + The Access-Control-Max-Age response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached. + + 1 + + + + + + Magento\Config\Model\Config\Source\Yesno + The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to frontend code when the request's credentials mode is include. + + 1 + + + +
+
+
diff --git a/app/code/Magento/GraphQl/etc/config.xml b/app/code/Magento/GraphQl/etc/config.xml new file mode 100644 index 0000000000000..39caacbec42d2 --- /dev/null +++ b/app/code/Magento/GraphQl/etc/config.xml @@ -0,0 +1,22 @@ + + + + + + + 0 + + + + 86400 + 0 + + + + diff --git a/app/code/Magento/GraphQl/etc/di.xml b/app/code/Magento/GraphQl/etc/di.xml index b356f33c4f4bf..fca6c425e2507 100644 --- a/app/code/Magento/GraphQl/etc/di.xml +++ b/app/code/Magento/GraphQl/etc/di.xml @@ -98,4 +98,31 @@ 300 + + + + + Access-Control-Max-Age + + + + + Access-Control-Allow-Credentials + + + + + Access-Control-Allow-Headers + + + + + Access-Control-Allow-Methods + + + + + Access-Control-Allow-Origin + + diff --git a/app/code/Magento/GraphQl/etc/graphql/di.xml b/app/code/Magento/GraphQl/etc/graphql/di.xml index 77fce336374dd..23d49124d1a02 100644 --- a/app/code/Magento/GraphQl/etc/graphql/di.xml +++ b/app/code/Magento/GraphQl/etc/graphql/di.xml @@ -30,4 +30,15 @@ + + + + Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowOriginHeaderProvider + Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowHeadersHeaderProvider + Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowMethodsHeaderProvider + Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowCredentialsHeaderProvider + Magento\GraphQl\Controller\HttpResponse\Cors\CorsMaxAgeHeaderProvider + + + diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php new file mode 100644 index 0000000000000..25c808a549e80 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php @@ -0,0 +1,126 @@ +resourceConfig = $objectManager->get(Config::class); + $this->reinitConfig = $objectManager->get(ReinitableConfigInterface::class); + $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 0); + $this->reinitConfig->reinit(); + } + + public function testNoCorsHeadersWhenCorsIsDisabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 0); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, 'Origin'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, '1'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, 'GET,POST'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, 'magento.local'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, '86400'); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + self::assertArrayNotHasKey('Access-Control-Max-Age', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Credentials', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Headers', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Methods', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Origin', $headers); + } + + public function testCorsHeadersWhenCorsIsEnabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 1); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, 'Origin'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, '1'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, 'GET,POST'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, 'magento.local'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, '86400'); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + self::assertEquals('Origin', $headers['Access-Control-Allow-Headers']); + self::assertEquals('1', $headers['Access-Control-Allow-Credentials']); + self::assertEquals('GET,POST', $headers['Access-Control-Allow-Methods']); + self::assertEquals('magento.local', $headers['Access-Control-Allow-Origin']); + self::assertEquals('86400', $headers['Access-Control-Max-Age']); + } + + public function testEmptyCorsHeadersWhenCorsIsEnabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 1); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, ''); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + self::assertArrayNotHasKey('Access-Control-Max-Age', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Credentials', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Headers', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Methods', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Origin', $headers); + } + + private function getHeadersFromIntrospectionQuery(): array + { + $query + = <<graphQlQueryWithResponseHeaders($query)['headers'] ?? []; + } +}