diff --git a/src/DependencyInjection/OverblogGraphQLExtension.php b/src/DependencyInjection/OverblogGraphQLExtension.php index 7191c3466..448d84ce6 100644 --- a/src/DependencyInjection/OverblogGraphQLExtension.php +++ b/src/DependencyInjection/OverblogGraphQLExtension.php @@ -182,15 +182,17 @@ private function setConfigBuilders(array $config, ContainerBuilder $container): private function setSecurity(array $config, ContainerBuilder $container): void { $executorDefinition = $container->getDefinition(Executor::class); - if ($config['security']['enable_introspection']) { - $executorDefinition->addMethodCall('enableIntrospectionQuery'); - } else { - $executorDefinition->addMethodCall('disableIntrospectionQuery'); - } foreach ($config['security'] as $key => $value) { $container->setParameter(sprintf('%s.%s', $this->getAlias(), $key), $value); } + + // Pass the parameter reference (not the compile-time value) so that an + // "enable_introspection" backed by an env variable is resolved at + // runtime instead of always evaluating truthy as a placeholder string. + $executorDefinition->addMethodCall('setIntrospectionQueryEnabled', [ + sprintf('%%%s.enable_introspection%%', $this->getAlias()), + ]); } private function setErrorHandler(array $config, ContainerBuilder $container): void diff --git a/src/Request/Executor.php b/src/Request/Executor.php index b4464a5e6..5d6936b44 100644 --- a/src/Request/Executor.php +++ b/src/Request/Executor.php @@ -117,12 +117,22 @@ public function setMaxQueryComplexity(int $maxQueryComplexity): void public function enableIntrospectionQuery(): void { - DocumentValidator::addRule(new DisableIntrospection(DisableIntrospection::DISABLED)); + $this->setIntrospectionQueryEnabled(true); } public function disableIntrospectionQuery(): void { - DocumentValidator::addRule(new DisableIntrospection(DisableIntrospection::ENABLED)); + $this->setIntrospectionQueryEnabled(false); + } + + /** + * Toggles the introspection query at runtime so the value can be provided by + * an environment variable (resolved when the service is instantiated) rather + * than being evaluated at container-compile time. + */ + public function setIntrospectionQueryEnabled(bool $enabled): void + { + DocumentValidator::addRule(new DisableIntrospection($enabled ? DisableIntrospection::DISABLED : DisableIntrospection::ENABLED)); } /** diff --git a/tests/Functional/App/config/enableIntrospectionEnvVar/config.yml b/tests/Functional/App/config/enableIntrospectionEnvVar/config.yml new file mode 100644 index 000000000..d05fb1425 --- /dev/null +++ b/tests/Functional/App/config/enableIntrospectionEnvVar/config.yml @@ -0,0 +1,17 @@ +imports: + - { resource: ../config.yml } + - { resource: ../connection/services.yml } + +overblog_graphql: + security: + enable_introspection: '%env(bool:GRAPHQL_ENABLE_INTROSPECTION)%' + definitions: + class_namespace: "Overblog\\GraphQLBundle\\IntrospectionEnvVar\\__DEFINITIONS__" + schema: + query: Query + mutation: ~ + mappings: + types: + - + type: yaml + dir: "%kernel.project_dir%/config/queryComplexity/mapping" diff --git a/tests/Functional/Security/EnableIntrospectionEnvVarTest.php b/tests/Functional/Security/EnableIntrospectionEnvVarTest.php new file mode 100644 index 000000000..0138d9d35 --- /dev/null +++ b/tests/Functional/Security/EnableIntrospectionEnvVarTest.php @@ -0,0 +1,100 @@ +setEnv(self::ENV_DISABLED); + + try { + $expected = [ + 'errors' => [ + [ + 'message' => 'GraphQL introspection is not allowed, but the query contained __schema or __type', + 'locations' => [ + [ + 'line' => 2, + 'column' => 3, + ], + ], + ], + ], + ]; + + $this->assertResponse($this->introspectionQuery, $expected, self::ANONYMOUS_USER, self::CONFIG_NAME); + } finally { + $this->restoreEnv($previous); + } + } + + public function testIntrospectionEnabledViaEnvVar(): void + { + $previous = getenv(self::ENV_VAR); + $this->setEnv(self::ENV_ENABLED); + + try { + $client = self::createClientAuthenticated(self::ANONYMOUS_USER, self::CONFIG_NAME); + $result = self::sendRequest($client, $this->introspectionQuery, true); + + static::assertArrayHasKey('data', $result); + static::assertArrayNotHasKey('errors', $result); + } finally { + $this->restoreEnv($previous); + } + } + + /** + * @param string|false $previous the value returned by getenv() before the test + */ + private function restoreEnv($previous): void + { + if (false === $previous) { + static::assertTrue(putenv(self::ENV_VAR)); + unset($_ENV[self::ENV_VAR], $_SERVER[self::ENV_VAR]); + } else { + static::assertTrue(putenv(self::ENV_VAR.'='.$previous)); + $_ENV[self::ENV_VAR] = $previous; + $_SERVER[self::ENV_VAR] = $previous; + } + } + + private function setEnv(string $value): void + { + static::assertTrue(putenv(self::ENV_VAR.'='.$value)); + $_ENV[self::ENV_VAR] = $value; + $_SERVER[self::ENV_VAR] = $value; + } +} diff --git a/tests/Request/ExecutorTest.php b/tests/Request/ExecutorTest.php index 802616607..f1008f62a 100644 --- a/tests/Request/ExecutorTest.php +++ b/tests/Request/ExecutorTest.php @@ -5,7 +5,11 @@ namespace Overblog\GraphQLBundle\Tests\Request; use GraphQL\Executor\Promise\Adapter\ReactPromiseAdapter; +use GraphQL\Language\Parser; +use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Schema; +use GraphQL\Validator\DocumentValidator; +use GraphQL\Validator\Rules\DisableIntrospection; use Overblog\GraphQLBundle\Executor\Executor; use Overblog\GraphQLBundle\Request\Executor as RequestExecutor; use PHPUnit\Framework\MockObject\MockObject; @@ -15,6 +19,8 @@ final class ExecutorTest extends TestCase { + private const INTROSPECTION_QUERY = '{ __schema { queryType { name } } }'; + protected function getMockedExecutor(): RequestExecutor { /** @var EventDispatcher&MockObject $dispatcher */ @@ -40,4 +46,30 @@ public function testGetSchemasName(): void $this->assertSame($executor->getSchemasNames(), ['schema1', 'schema2']); } + + public function testIntrospectionCompatibilityMethodsToggleValidation(): void + { + $executor = $this->getMockedExecutor(); + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'value' => ['type' => 'String'], + ], + ]), + ]); + $document = Parser::parse(self::INTROSPECTION_QUERY); + + try { + $executor->disableIntrospectionQuery(); + $errors = DocumentValidator::validate($schema, $document); + self::assertCount(1, $errors); + self::assertSame(DisableIntrospection::introspectionDisabledMessage(), $errors[0]->getMessage()); + + $executor->enableIntrospectionQuery(); + self::assertSame([], DocumentValidator::validate($schema, $document)); + } finally { + $executor->enableIntrospectionQuery(); + } + } }