From 9bdf79f5133c782af356d4ca66f3c2872281b71f Mon Sep 17 00:00:00 2001 From: Perry Janssen Date: Fri, 17 Sep 2021 14:13:14 +0200 Subject: [PATCH] feat: Add configurable validation rules --- config/schema/graphql.schema.yml | 12 +++++ graphql.services.yml | 6 +++ src/Entity/Server.php | 25 +++++++++- .../ExplorerEventSubscriber.php | 50 +++++++++++++++++++ src/Form/ServerForm.php | 33 ++++++++++++ 5 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 src/EventSubscriber/ExplorerEventSubscriber.php diff --git a/config/schema/graphql.schema.yml b/config/schema/graphql.schema.yml index fd3657a7a..3ffaf69b3 100644 --- a/config/schema/graphql.schema.yml +++ b/config/schema/graphql.schema.yml @@ -23,6 +23,18 @@ graphql.graphql_servers.*: batching: type: boolean label: 'Batching' + disable_introspection: + type: boolean + label: 'Disable Introspection' + depth: + type: number + label: 'Max query depth' + complexity: + type: number + label: 'Max query complexity' + bypass_validation_token: + type: string + label: 'Bypass validation token' schema_configuration: type: 'graphql.schema.[%parent.schema]' persisted_queries_settings: diff --git a/graphql.services.yml b/graphql.services.yml index 8de9f5448..8063d2ae1 100644 --- a/graphql.services.yml +++ b/graphql.services.yml @@ -108,6 +108,12 @@ services: tags: - { name: event_subscriber } + # Reset the current language during operations. + graphql.explorer_subscriber: + class: Drupal\graphql\EventSubscriber\ExplorerEventSubscriber + tags: + - { name: event_subscriber } + # Plugin manager for schemas plugin.manager.graphql.schema: class: Drupal\graphql\Plugin\SchemaPluginManager diff --git a/src/Entity/Server.php b/src/Entity/Server.php index e569ea343..9659f3325 100644 --- a/src/Entity/Server.php +++ b/src/Entity/Server.php @@ -25,6 +25,9 @@ use GraphQL\Server\Helper; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Validator\DocumentValidator; +use GraphQL\Validator\Rules\DisableIntrospection; +use GraphQL\Validator\Rules\QueryComplexity; +use GraphQL\Validator\Rules\QueryDepth; /** * The main GraphQL configuration and request entry point. @@ -59,7 +62,11 @@ * "endpoint", * "debug_flag", * "caching", - * "batching" + * "batching", + * "disable_introspection", + * "depth", + * "complexity", + * "bypass_validation_token" * }, * links = { * "collection" = "/admin/config/graphql/servers", @@ -498,7 +505,21 @@ protected function getValidationRules() { return []; } - return array_values(DocumentValidator::defaultRules()); + $rules = array_values(DocumentValidator::defaultRules()); + + if (\Drupal::request()->get('bypass_validation') !== $this->get('bypass_validation_token')) { + if ($this->get('disable_introspection')) { + $rules[DisableIntrospection::class] = new DisableIntrospection(); + } + if ($this->get('depth')) { + $rules[QueryDepth::class] = new QueryDepth($this->get('depth')); + } + if ($this->get('complexity')) { + $rules[QueryComplexity::class] = new QueryComplexity($this->get('complexity')); + } + } + + return $rules; }; } diff --git a/src/EventSubscriber/ExplorerEventSubscriber.php b/src/EventSubscriber/ExplorerEventSubscriber.php new file mode 100644 index 000000000..3d23de803 --- /dev/null +++ b/src/EventSubscriber/ExplorerEventSubscriber.php @@ -0,0 +1,50 @@ +getRouteName(); + + // If a bypass_validation param is already set, skip. + if ($event->getRequest()->get('bypass_validation')) { + return; + } + + // Only bypass validation for these two routes. + if ($route === 'graphql.explorer' || $route === 'graphql.voyager') { + /** @var \Drupal\graphql\Entity\Server $server */ + $server = $event->getRequest()->get('graphql_server'); + + $url = $event->getRequest()->getUri(); + + // Get the bypass_validation_token from the server settings. + $bypass_validation_token = $server->get('bypass_validation_token'); + + // Add the bypass_validation parameter to the current url. + $url .= (parse_url($url, PHP_URL_QUERY) ? '&' : '?') . 'bypass_validation=' . $bypass_validation_token; + + // Redirect to the new url. + $event->setResponse(new RedirectResponse($url)); + } + } +} diff --git a/src/Form/ServerForm.php b/src/Form/ServerForm.php index d8876765b..b2ffd7e0d 100644 --- a/src/Form/ServerForm.php +++ b/src/Form/ServerForm.php @@ -186,6 +186,39 @@ public function form(array $form, FormStateInterface $formState): array { '#description' => $this->t('Whether caching of queries and partial results is enabled.'), ]; + $form['validation'] = [ + '#title' => $this->t('Validation rules'), + '#type' => 'fieldset', + ]; + + $form['validation']['disable_introspection'] = [ + '#title' => $this->t('Disable introspection'), + '#type' => 'checkbox', + '#default_value' => !!$server->get('disable_introspection'), + '#description' => $this->t('Whether introspection should be disabled.'), + ]; + + $form['validation']['depth'] = [ + '#title' => $this->t('Max query depth'), + '#type' => 'number', + '#default_value' => $server->get('depth'), + '#description' => $this->t('The maximum allowed depth of nested queries. Leave empty to set unlimited.'), + ]; + + $form['validation']['complexity'] = [ + '#title' => $this->t('Max query complexity'), + '#default_value' => $server->get('complexity'), + '#type' => 'number', + '#description' => $this->t('The maximum allowed complexity of a query. Leave empty to set unlimited.'), + ]; + + $form['validation']['bypass_validation_token'] = [ + '#title' => $this->t('Bypass validation token'), + '#default_value' => $server->get('bypass_validation_token'), + '#type' => 'textfield', + '#description' => $this->t('A string token that can be used as the "bypass_validation" parameter when doing a GraphQL request. This bypasses the above validation rules. Could be used when generating types for front-end applications.'), + ]; + $debug_flags = $server->get('debug_flag') ?? 0; $form['debug_flag'] = [ '#title' => $this->t('Debug settings'),