From aa11681048a6be67023627a4907e013d65dd13d1 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Fri, 13 Aug 2021 18:22:12 +0530 Subject: [PATCH 01/23] add depth limit validator --- docs/execution/index.rst | 1 + docs/execution/validators.rst | 35 +++ graphene/validators/__init__.py | 6 + graphene/validators/depth_limit_validator.py | 198 +++++++++++++ graphene/validators/tests/__init__.py | 0 .../tests/test_depth_limit_validator.py | 279 ++++++++++++++++++ 6 files changed, 519 insertions(+) create mode 100644 docs/execution/validators.rst create mode 100644 graphene/validators/__init__.py create mode 100644 graphene/validators/depth_limit_validator.py create mode 100644 graphene/validators/tests/__init__.py create mode 100644 graphene/validators/tests/test_depth_limit_validator.py diff --git a/docs/execution/index.rst b/docs/execution/index.rst index dbfbfa726..466526657 100644 --- a/docs/execution/index.rst +++ b/docs/execution/index.rst @@ -10,3 +10,4 @@ Execution dataloader fileuploading subscriptions + validators diff --git a/docs/execution/validators.rst b/docs/execution/validators.rst new file mode 100644 index 000000000..94bb7c2fc --- /dev/null +++ b/docs/execution/validators.rst @@ -0,0 +1,35 @@ +Middleware +========== + +Validation rules help validate a given GraphQL query, before executing it.To help with common use +cases, graphene provides a few validation rules out of the box. + + +Depth limit Validator +----------------- +The depth limit validator helps to prevent execution of malicious +queries. It takes in the following arguments. + +- ``max_depth`` is the maximum allowed depth for any operation in a GraphQL document. +- ``ignore`` Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that returns a boolean +- ``callback`` Called each time validation runs. Receives an Object which is a map of the depths for each operation. + +Example +------- + +Here is how you would implement depth-limiting on your schema. + +.. code:: python + from graphene.validators import depth_limit_validator + + # The following schema doesn't execute queries + # which have a depth more than 20. + + result = schema.execute( + 'THE QUERY', + validation_rules=[ + depth_limit_validator( + max_depth=20 + ) + ] + ) diff --git a/graphene/validators/__init__.py b/graphene/validators/__init__.py new file mode 100644 index 000000000..8bd8d884f --- /dev/null +++ b/graphene/validators/__init__.py @@ -0,0 +1,6 @@ +from .depth_limit_validator import depth_limit_validator + + +__all__ = [ + "depth_limit_validator" +] diff --git a/graphene/validators/depth_limit_validator.py b/graphene/validators/depth_limit_validator.py new file mode 100644 index 000000000..436152055 --- /dev/null +++ b/graphene/validators/depth_limit_validator.py @@ -0,0 +1,198 @@ +# This is a Python port of https://github.com/stems/graphql-depth-limit +# which is licensed under the terms of the MIT license, reproduced below. +# +# ----------- +# +# MIT License +# +# Copyright (c) 2017 Stem +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import re +from typing import Callable, Dict, List, Optional, Union + +from graphql import GraphQLError, is_introspection_type +from graphql.language import ( + DefinitionNode, + FieldNode, + FragmentDefinitionNode, + FragmentSpreadNode, + InlineFragmentNode, + Node, + OperationDefinitionNode, +) +from graphql.validation import ValidationContext, ValidationRule + + +IgnoreType = Union[Callable[[str], bool], re.Pattern, str] + + +def depth_limit_validator( + max_depth: int, + ignore: Optional[List[IgnoreType]] = None, + callback: Callable[[Dict[str, int]], None] = None, +): + class DepthLimitValidator(ValidationRule): + def __init__(self, validation_context: ValidationContext): + document = validation_context.document + definitions = document.definitions + + fragments = get_fragments(definitions) + queries = get_queries_and_mutations(definitions) + query_depths = {} + + for name in queries: + query_depths[name] = determine_depth( + node=queries[name], + fragments=fragments, + depth_so_far=0, + max_depth=max_depth, + context=validation_context, + operation_name=name, + ignore=ignore, + ) + + if callable(callback): + callback(query_depths) + super().__init__(validation_context) + + return DepthLimitValidator + + +def get_fragments( + definitions: List[DefinitionNode], +) -> Dict[str, FragmentDefinitionNode]: + fragments = {} + for definition in definitions: + if isinstance(definition, FragmentDefinitionNode): + fragments[definition.name.value] = definition + + return fragments + + +# This will actually get both queries and mutations. +# We can basically treat those the same +def get_queries_and_mutations( + definitions: List[DefinitionNode], +) -> Dict[str, OperationDefinitionNode]: + operations = {} + + for definition in definitions: + if isinstance(definition, OperationDefinitionNode): + operation = definition.name.value if definition.name else "anonymous" + operations[operation] = definition + + return operations + + +def determine_depth( + node: Node, + fragments: Dict[str, FragmentDefinitionNode], + depth_so_far: int, + max_depth: int, + context: ValidationContext, + operation_name: str, + ignore: Optional[List[IgnoreType]] = None, +) -> int: + if depth_so_far > max_depth: + context.report_error( + GraphQLError( + f"'{operation_name}' exceeds maximum operation depth of {max_depth}", + [node], + ) + ) + return depth_so_far + + if isinstance(node, FieldNode): + # from: https://spec.graphql.org/June2018/#sec-Schema + # > All types and directives defined within a schema must not have a name which + # > begins with "__" (two underscores), as this is used exclusively + # > by GraphQL’s introspection system. + should_ignore = str(node.name.value).startswith("__") or is_ignored( + node, ignore + ) + + if should_ignore or not node.selection_set: + return 0 + + return 1 + max( + map( + lambda selection: determine_depth( + node=selection, + fragments=fragments, + depth_so_far=depth_so_far + 1, + max_depth=max_depth, + context=context, + operation_name=operation_name, + ignore=ignore, + ), + node.selection_set.selections, + ) + ) + elif isinstance(node, FragmentSpreadNode): + return determine_depth( + node=fragments[node.name.value], + fragments=fragments, + depth_so_far=depth_so_far, + max_depth=max_depth, + context=context, + operation_name=operation_name, + ignore=ignore, + ) + elif isinstance( + node, (InlineFragmentNode, FragmentDefinitionNode, OperationDefinitionNode) + ): + return max( + map( + lambda selection: determine_depth( + node=selection, + fragments=fragments, + depth_so_far=depth_so_far, + max_depth=max_depth, + context=context, + operation_name=operation_name, + ignore=ignore, + ), + node.selection_set.selections, + ) + ) + else: + raise Exception(f"Depth crawler cannot handle: {node.kind}") # pragma: no cover + + +def is_ignored(node: FieldNode, ignore: Optional[List[IgnoreType]] = None) -> bool: + if ignore is None: + return False + + for rule in ignore: + field_name = node.name.value + if isinstance(rule, str): + if field_name == rule: + return True + elif isinstance(rule, re.Pattern): + if rule.match(field_name): + return True + elif callable(rule): + if rule(field_name): + return True + else: + raise ValueError(f"Invalid ignore option: {rule}") + + return False diff --git a/graphene/validators/tests/__init__.py b/graphene/validators/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/graphene/validators/tests/test_depth_limit_validator.py b/graphene/validators/tests/test_depth_limit_validator.py new file mode 100644 index 000000000..ea50c8d49 --- /dev/null +++ b/graphene/validators/tests/test_depth_limit_validator.py @@ -0,0 +1,279 @@ +import re + +from pytest import raises +from graphql import parse, get_introspection_query, validate + +from ...types import Schema, ObjectType, Interface +from ...types import String, Int, List, Field +from ..depth_limit_validator import depth_limit_validator + + +class PetType(Interface): + name = String(required=True) + + class meta: + name = "Pet" + + +class CatType(ObjectType): + class meta: + name = "Cat" + interfaces = (PetType,) + + +class DogType(ObjectType): + class meta: + name = "Dog" + interfaces = (PetType,) + + +class AddressType(ObjectType): + street = String(required=True) + number = Int(required=True) + city = String(required=True) + country = String(required=True) + + class Meta: + name = "Address" + + +class HumanType(ObjectType): + name = String(required=True) + email = String(required=True) + address = Field(AddressType, required=True) + pets = List(PetType, required=True) + + class Meta: + name = "Human" + + +class Query(ObjectType): + user = Field( + HumanType, + required=True, + name=String() + ) + version = String( + required=True + ) + user1 = Field( + HumanType, + required=True + ) + user2 = Field( + HumanType, + required=True + ) + user3 = Field( + HumanType, + required=True + ) + + @staticmethod + def resolve_user(root, info, name=None): + pass + + +schema = Schema(query=Query) + + +def run_query(query: str, max_depth: int, ignore=None): + document = parse(query) + + result = None + + def callback(query_depths): + nonlocal result + result = query_depths + + errors = validate( + schema.graphql_schema, + document, + rules=( + depth_limit_validator( + max_depth=max_depth, + ignore=ignore, + callback=callback + ), + ), + ) + + return errors, result + + +def test_should_count_depth_without_fragment(): + query = """ + query read0 { + version + } + query read1 { + version + user { + name + } + } + query read2 { + matt: user(name: "matt") { + email + } + andy: user(name: "andy") { + email + address { + city + } + } + } + query read3 { + matt: user(name: "matt") { + email + } + andy: user(name: "andy") { + email + address { + city + } + pets { + name + owner { + name + } + } + } + } + """ + + expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} + + errors, result = run_query(query, 10) + assert not errors + assert result == expected + + +def test_should_count_with_fragments(): + query = """ + query read0 { + ... on Query { + version + } + } + query read1 { + version + user { + ... on Human { + name + } + } + } + fragment humanInfo on Human { + email + } + fragment petInfo on Pet { + name + owner { + name + } + } + query read2 { + matt: user(name: "matt") { + ...humanInfo + } + andy: user(name: "andy") { + ...humanInfo + address { + city + } + } + } + query read3 { + matt: user(name: "matt") { + ...humanInfo + } + andy: user(name: "andy") { + ... on Human { + email + } + address { + city + } + pets { + ...petInfo + } + } + } + """ + + expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} + + errors, result = run_query(query, 10) + assert not errors + assert result == expected + + +def test_should_ignore_the_introspection_query(): + errors, result = run_query(get_introspection_query(), 10) + assert not errors + assert result == {"IntrospectionQuery": 0} + + +def test_should_catch_very_deep_query(): + query = """{ + user { + pets { + owner { + pets { + owner { + pets { + name + } + } + } + } + } + } + } + """ + errors, result = run_query(query, 4) + + assert len(errors) == 1 + assert errors[0].message == "'anonymous' exceeds maximum operation depth of 4" + + +def test_should_ignore_field(): + query = """ + query read1 { + user { address { city } } + } + query read2 { + user1 { address { city } } + user2 { address { city } } + user3 { address { city } } + } + """ + + errors, result = run_query( + query, + 10, + ignore=[ + "user1", + re.compile("user2"), + lambda field_name: field_name == "user3", + ], + ) + + expected = {"read1": 2, "read2": 0} + assert not errors + assert result == expected + + +def test_should_raise_invalid_ignore(): + query = """ + query read1 { + user { address { city } } + } + """ + with raises(ValueError, match="Invalid ignore option:"): + run_query( + query, + 10, + ignore=[True], + ) From fc2967e276bb78a4b388feaa091c2f9bc1f31ca2 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Fri, 13 Aug 2021 18:51:23 +0530 Subject: [PATCH 02/23] remove unused imports --- graphene/validators/depth_limit_validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/validators/depth_limit_validator.py b/graphene/validators/depth_limit_validator.py index 436152055..d25897006 100644 --- a/graphene/validators/depth_limit_validator.py +++ b/graphene/validators/depth_limit_validator.py @@ -28,7 +28,7 @@ import re from typing import Callable, Dict, List, Optional, Union -from graphql import GraphQLError, is_introspection_type +from graphql import GraphQLError from graphql.language import ( DefinitionNode, FieldNode, From 4259502dc373c8d3a4d6463696441e4bd5a0cc68 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Fri, 13 Aug 2021 20:02:20 +0530 Subject: [PATCH 03/23] update docs --- docs/execution/validators.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/execution/validators.rst b/docs/execution/validators.rst index 94bb7c2fc..f1cfac882 100644 --- a/docs/execution/validators.rst +++ b/docs/execution/validators.rst @@ -1,4 +1,4 @@ -Middleware +Validators ========== Validation rules help validate a given GraphQL query, before executing it.To help with common use From 5977b1648ce75730a3a494aeeef9df43fc5f2330 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Fri, 13 Aug 2021 20:04:42 +0530 Subject: [PATCH 04/23] fix typo --- docs/execution/validators.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/execution/validators.rst b/docs/execution/validators.rst index f1cfac882..a37c80ab4 100644 --- a/docs/execution/validators.rst +++ b/docs/execution/validators.rst @@ -1,7 +1,7 @@ Validators ========== -Validation rules help validate a given GraphQL query, before executing it.To help with common use +Validation rules help validate a given GraphQL query, before executing it. To help with common use cases, graphene provides a few validation rules out of the box. From a784ef15e59851afa804162a200b9c80a11c200c Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Fri, 13 Aug 2021 20:24:53 +0530 Subject: [PATCH 05/23] add disable introspection --- docs/execution/validators.rst | 2 +- graphene/utils/is_introspection_key.py | 6 + graphene/validation/__init__.py | 8 + .../depth_limit.py} | 10 +- graphene/validation/disable_introspection.py | 22 ++ .../tests/__init__.py | 0 .../tests/test_disable_introspection.py | 33 +++ graphene/validators/__init__.py | 6 - .../tests/test_depth_limit_validator.py | 279 ------------------ 9 files changed, 74 insertions(+), 292 deletions(-) create mode 100644 graphene/utils/is_introspection_key.py create mode 100644 graphene/validation/__init__.py rename graphene/{validators/depth_limit_validator.py => validation/depth_limit.py} (94%) create mode 100644 graphene/validation/disable_introspection.py rename graphene/{validators => validation}/tests/__init__.py (100%) create mode 100644 graphene/validation/tests/test_disable_introspection.py delete mode 100644 graphene/validators/__init__.py delete mode 100644 graphene/validators/tests/test_depth_limit_validator.py diff --git a/docs/execution/validators.rst b/docs/execution/validators.rst index a37c80ab4..92b8ecd2b 100644 --- a/docs/execution/validators.rst +++ b/docs/execution/validators.rst @@ -20,7 +20,7 @@ Example Here is how you would implement depth-limiting on your schema. .. code:: python - from graphene.validators import depth_limit_validator + from graphene.validation import depth_limit_validator # The following schema doesn't execute queries # which have a depth more than 20. diff --git a/graphene/utils/is_introspection_key.py b/graphene/utils/is_introspection_key.py new file mode 100644 index 000000000..689519404 --- /dev/null +++ b/graphene/utils/is_introspection_key.py @@ -0,0 +1,6 @@ +def is_introspection_key(key): + # from: https://spec.graphql.org/June2018/#sec-Schema + # > All types and directives defined within a schema must not have a name which + # > begins with "__" (two underscores), as this is used exclusively + # > by GraphQL’s introspection system. + return str(node.name.value).startswith("__") diff --git a/graphene/validation/__init__.py b/graphene/validation/__init__.py new file mode 100644 index 000000000..03e4605c8 --- /dev/null +++ b/graphene/validation/__init__.py @@ -0,0 +1,8 @@ +from .depth_limit import depth_limit_validator +from .disable_introspection import disable_introspection + + +__all__ = [ + "depth_limit_validator", + "disable_introspection" +] diff --git a/graphene/validators/depth_limit_validator.py b/graphene/validation/depth_limit.py similarity index 94% rename from graphene/validators/depth_limit_validator.py rename to graphene/validation/depth_limit.py index d25897006..4136555d7 100644 --- a/graphene/validators/depth_limit_validator.py +++ b/graphene/validation/depth_limit.py @@ -29,6 +29,7 @@ from typing import Callable, Dict, List, Optional, Union from graphql import GraphQLError +from graphql.validation import ValidationContext, ValidationRule from graphql.language import ( DefinitionNode, FieldNode, @@ -38,7 +39,8 @@ Node, OperationDefinitionNode, ) -from graphql.validation import ValidationContext, ValidationRule + +from ..utils.is_introspection_key import is_introspection_key IgnoreType = Union[Callable[[str], bool], re.Pattern, str] @@ -121,11 +123,7 @@ def determine_depth( return depth_so_far if isinstance(node, FieldNode): - # from: https://spec.graphql.org/June2018/#sec-Schema - # > All types and directives defined within a schema must not have a name which - # > begins with "__" (two underscores), as this is used exclusively - # > by GraphQL’s introspection system. - should_ignore = str(node.name.value).startswith("__") or is_ignored( + should_ignore = is_introspection_key(node.name.value) or is_ignored( node, ignore ) diff --git a/graphene/validation/disable_introspection.py b/graphene/validation/disable_introspection.py new file mode 100644 index 000000000..eb24be554 --- /dev/null +++ b/graphene/validation/disable_introspection.py @@ -0,0 +1,22 @@ +from graphql import GraphQLError +from graphql.language import FieldNode +from graphql.validation import ValidationRule + +from ..utils.is_introspection_key import is_introspection_key + + +def disable_introspection(): + class DisableIntrospection(ValidationRule): + def enter_field(self, node: FieldNode, *_args): + field_name = node.name.value + if not is_introspection_key(field_name): + return + + self.report_error( + GraphQLError( + f"Cannot query '{field_name}': introspection is disabled.", + node, + ) + ) + + return DisableIntrospection diff --git a/graphene/validators/tests/__init__.py b/graphene/validation/tests/__init__.py similarity index 100% rename from graphene/validators/tests/__init__.py rename to graphene/validation/tests/__init__.py diff --git a/graphene/validation/tests/test_disable_introspection.py b/graphene/validation/tests/test_disable_introspection.py new file mode 100644 index 000000000..4d1faa7d2 --- /dev/null +++ b/graphene/validation/tests/test_disable_introspection.py @@ -0,0 +1,33 @@ +from graphql import parse, validate + +from ...types import Schema, ObjectType, String +from ..disable_introspection import disable_introspection + + +class Query(ObjectType): + name = String( + required=True + ) + + +schema = Schema(query=Query) + + +def run_query(query: str): + document = parse(query) + + result = None + + def callback(query_depths): + nonlocal result + result = query_depths + + errors = validate( + schema.graphql_schema, + document, + rules=( + disable_introspection(), + ), + ) + + return errors, result diff --git a/graphene/validators/__init__.py b/graphene/validators/__init__.py deleted file mode 100644 index 8bd8d884f..000000000 --- a/graphene/validators/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .depth_limit_validator import depth_limit_validator - - -__all__ = [ - "depth_limit_validator" -] diff --git a/graphene/validators/tests/test_depth_limit_validator.py b/graphene/validators/tests/test_depth_limit_validator.py deleted file mode 100644 index ea50c8d49..000000000 --- a/graphene/validators/tests/test_depth_limit_validator.py +++ /dev/null @@ -1,279 +0,0 @@ -import re - -from pytest import raises -from graphql import parse, get_introspection_query, validate - -from ...types import Schema, ObjectType, Interface -from ...types import String, Int, List, Field -from ..depth_limit_validator import depth_limit_validator - - -class PetType(Interface): - name = String(required=True) - - class meta: - name = "Pet" - - -class CatType(ObjectType): - class meta: - name = "Cat" - interfaces = (PetType,) - - -class DogType(ObjectType): - class meta: - name = "Dog" - interfaces = (PetType,) - - -class AddressType(ObjectType): - street = String(required=True) - number = Int(required=True) - city = String(required=True) - country = String(required=True) - - class Meta: - name = "Address" - - -class HumanType(ObjectType): - name = String(required=True) - email = String(required=True) - address = Field(AddressType, required=True) - pets = List(PetType, required=True) - - class Meta: - name = "Human" - - -class Query(ObjectType): - user = Field( - HumanType, - required=True, - name=String() - ) - version = String( - required=True - ) - user1 = Field( - HumanType, - required=True - ) - user2 = Field( - HumanType, - required=True - ) - user3 = Field( - HumanType, - required=True - ) - - @staticmethod - def resolve_user(root, info, name=None): - pass - - -schema = Schema(query=Query) - - -def run_query(query: str, max_depth: int, ignore=None): - document = parse(query) - - result = None - - def callback(query_depths): - nonlocal result - result = query_depths - - errors = validate( - schema.graphql_schema, - document, - rules=( - depth_limit_validator( - max_depth=max_depth, - ignore=ignore, - callback=callback - ), - ), - ) - - return errors, result - - -def test_should_count_depth_without_fragment(): - query = """ - query read0 { - version - } - query read1 { - version - user { - name - } - } - query read2 { - matt: user(name: "matt") { - email - } - andy: user(name: "andy") { - email - address { - city - } - } - } - query read3 { - matt: user(name: "matt") { - email - } - andy: user(name: "andy") { - email - address { - city - } - pets { - name - owner { - name - } - } - } - } - """ - - expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} - - errors, result = run_query(query, 10) - assert not errors - assert result == expected - - -def test_should_count_with_fragments(): - query = """ - query read0 { - ... on Query { - version - } - } - query read1 { - version - user { - ... on Human { - name - } - } - } - fragment humanInfo on Human { - email - } - fragment petInfo on Pet { - name - owner { - name - } - } - query read2 { - matt: user(name: "matt") { - ...humanInfo - } - andy: user(name: "andy") { - ...humanInfo - address { - city - } - } - } - query read3 { - matt: user(name: "matt") { - ...humanInfo - } - andy: user(name: "andy") { - ... on Human { - email - } - address { - city - } - pets { - ...petInfo - } - } - } - """ - - expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} - - errors, result = run_query(query, 10) - assert not errors - assert result == expected - - -def test_should_ignore_the_introspection_query(): - errors, result = run_query(get_introspection_query(), 10) - assert not errors - assert result == {"IntrospectionQuery": 0} - - -def test_should_catch_very_deep_query(): - query = """{ - user { - pets { - owner { - pets { - owner { - pets { - name - } - } - } - } - } - } - } - """ - errors, result = run_query(query, 4) - - assert len(errors) == 1 - assert errors[0].message == "'anonymous' exceeds maximum operation depth of 4" - - -def test_should_ignore_field(): - query = """ - query read1 { - user { address { city } } - } - query read2 { - user1 { address { city } } - user2 { address { city } } - user3 { address { city } } - } - """ - - errors, result = run_query( - query, - 10, - ignore=[ - "user1", - re.compile("user2"), - lambda field_name: field_name == "user3", - ], - ) - - expected = {"read1": 2, "read2": 0} - assert not errors - assert result == expected - - -def test_should_raise_invalid_ignore(): - query = """ - query read1 { - user { address { city } } - } - """ - with raises(ValueError, match="Invalid ignore option:"): - run_query( - query, - 10, - ignore=[True], - ) From d7b474751d59b9c94279283295057f224b7688a7 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Sat, 14 Aug 2021 07:45:34 +0530 Subject: [PATCH 06/23] add depth limit validator tests --- docs/execution/validators.rst | 24 +- graphene/utils/is_introspection_key.py | 2 +- .../tests/test_depth_limit_validator.py | 279 ++++++++++++++++++ .../tests/test_disable_introspection.py | 4 - 4 files changed, 297 insertions(+), 12 deletions(-) create mode 100644 graphene/validation/tests/test_depth_limit_validator.py diff --git a/docs/execution/validators.rst b/docs/execution/validators.rst index 92b8ecd2b..d7e1310b1 100644 --- a/docs/execution/validators.rst +++ b/docs/execution/validators.rst @@ -20,16 +20,26 @@ Example Here is how you would implement depth-limiting on your schema. .. code:: python + from graphql import validate + from graphene import ObjectType, Schema, String from graphene.validation import depth_limit_validator - # The following schema doesn't execute queries - # which have a depth more than 20. - result = schema.execute( - 'THE QUERY', - validation_rules=[ + class MyQuery(ObjectType): + name = String(required=True) + + + schema = Schema(query=MyQuery) + + # Queries which have a depth more than 20 + # will not be executed. + + validation_errors = validate( + schema=schema, + document='THE QUERY', + rules=( depth_limit_validator( max_depth=20 - ) - ] + ), + ) ) diff --git a/graphene/utils/is_introspection_key.py b/graphene/utils/is_introspection_key.py index 689519404..59d72b24c 100644 --- a/graphene/utils/is_introspection_key.py +++ b/graphene/utils/is_introspection_key.py @@ -3,4 +3,4 @@ def is_introspection_key(key): # > All types and directives defined within a schema must not have a name which # > begins with "__" (two underscores), as this is used exclusively # > by GraphQL’s introspection system. - return str(node.name.value).startswith("__") + return str(key).startswith("__") diff --git a/graphene/validation/tests/test_depth_limit_validator.py b/graphene/validation/tests/test_depth_limit_validator.py new file mode 100644 index 000000000..3eea3a32c --- /dev/null +++ b/graphene/validation/tests/test_depth_limit_validator.py @@ -0,0 +1,279 @@ +import re + +from pytest import raises +from graphql import parse, get_introspection_query, validate + +from ...types import Schema, ObjectType, Interface +from ...types import String, Int, List, Field +from ..depth_limit import depth_limit_validator + + +class PetType(Interface): + name = String(required=True) + + class meta: + name = "Pet" + + +class CatType(ObjectType): + class meta: + name = "Cat" + interfaces = (PetType,) + + +class DogType(ObjectType): + class meta: + name = "Dog" + interfaces = (PetType,) + + +class AddressType(ObjectType): + street = String(required=True) + number = Int(required=True) + city = String(required=True) + country = String(required=True) + + class Meta: + name = "Address" + + +class HumanType(ObjectType): + name = String(required=True) + email = String(required=True) + address = Field(AddressType, required=True) + pets = List(PetType, required=True) + + class Meta: + name = "Human" + + +class Query(ObjectType): + user = Field( + HumanType, + required=True, + name=String() + ) + version = String( + required=True + ) + user1 = Field( + HumanType, + required=True + ) + user2 = Field( + HumanType, + required=True + ) + user3 = Field( + HumanType, + required=True + ) + + @staticmethod + def resolve_user(root, info, name=None): + pass + + +schema = Schema(query=Query) + + +def run_query(query: str, max_depth: int, ignore=None): + document = parse(query) + + result = None + + def callback(query_depths): + nonlocal result + result = query_depths + + errors = validate( + schema.graphql_schema, + document, + rules=( + depth_limit_validator( + max_depth=max_depth, + ignore=ignore, + callback=callback + ), + ), + ) + + return errors, result + + +def test_should_count_depth_without_fragment(): + query = """ + query read0 { + version + } + query read1 { + version + user { + name + } + } + query read2 { + matt: user(name: "matt") { + email + } + andy: user(name: "andy") { + email + address { + city + } + } + } + query read3 { + matt: user(name: "matt") { + email + } + andy: user(name: "andy") { + email + address { + city + } + pets { + name + owner { + name + } + } + } + } + """ + + expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} + + errors, result = run_query(query, 10) + assert not errors + assert result == expected + + +def test_should_count_with_fragments(): + query = """ + query read0 { + ... on Query { + version + } + } + query read1 { + version + user { + ... on Human { + name + } + } + } + fragment humanInfo on Human { + email + } + fragment petInfo on Pet { + name + owner { + name + } + } + query read2 { + matt: user(name: "matt") { + ...humanInfo + } + andy: user(name: "andy") { + ...humanInfo + address { + city + } + } + } + query read3 { + matt: user(name: "matt") { + ...humanInfo + } + andy: user(name: "andy") { + ... on Human { + email + } + address { + city + } + pets { + ...petInfo + } + } + } + """ + + expected = {"read0": 0, "read1": 1, "read2": 2, "read3": 3} + + errors, result = run_query(query, 10) + assert not errors + assert result == expected + + +def test_should_ignore_the_introspection_query(): + errors, result = run_query(get_introspection_query(), 10) + assert not errors + assert result == {"IntrospectionQuery": 0} + + +def test_should_catch_very_deep_query(): + query = """{ + user { + pets { + owner { + pets { + owner { + pets { + name + } + } + } + } + } + } + } + """ + errors, result = run_query(query, 4) + + assert len(errors) == 1 + assert errors[0].message == "'anonymous' exceeds maximum operation depth of 4" + + +def test_should_ignore_field(): + query = """ + query read1 { + user { address { city } } + } + query read2 { + user1 { address { city } } + user2 { address { city } } + user3 { address { city } } + } + """ + + errors, result = run_query( + query, + 10, + ignore=[ + "user1", + re.compile("user2"), + lambda field_name: field_name == "user3", + ], + ) + + expected = {"read1": 2, "read2": 0} + assert not errors + assert result == expected + + +def test_should_raise_invalid_ignore(): + query = """ + query read1 { + user { address { city } } + } + """ + with raises(ValueError, match="Invalid ignore option:"): + run_query( + query, + 10, + ignore=[True], + ) diff --git a/graphene/validation/tests/test_disable_introspection.py b/graphene/validation/tests/test_disable_introspection.py index 4d1faa7d2..c13786ed8 100644 --- a/graphene/validation/tests/test_disable_introspection.py +++ b/graphene/validation/tests/test_disable_introspection.py @@ -18,10 +18,6 @@ def run_query(query: str): result = None - def callback(query_depths): - nonlocal result - result = query_depths - errors = validate( schema.graphql_schema, document, From 7be4bd6bc6a916f2c4f2ecc7cf184064dc2c8f19 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Sat, 14 Aug 2021 07:49:09 +0530 Subject: [PATCH 07/23] update docs --- docs/execution/index.rst | 2 +- docs/execution/{validators.rst => validation.rst} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/execution/{validators.rst => validation.rst} (99%) diff --git a/docs/execution/index.rst b/docs/execution/index.rst index 466526657..f775cc007 100644 --- a/docs/execution/index.rst +++ b/docs/execution/index.rst @@ -10,4 +10,4 @@ Execution dataloader fileuploading subscriptions - validators + validation diff --git a/docs/execution/validators.rst b/docs/execution/validation.rst similarity index 99% rename from docs/execution/validators.rst rename to docs/execution/validation.rst index d7e1310b1..ac27ec438 100644 --- a/docs/execution/validators.rst +++ b/docs/execution/validation.rst @@ -1,4 +1,4 @@ -Validators +Validation ========== Validation rules help validate a given GraphQL query, before executing it. To help with common use From ac5dd90f5fc610a37af7fc27efd022d15ebe821f Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Sat, 14 Aug 2021 07:54:58 +0530 Subject: [PATCH 08/23] fix typo in docs --- docs/execution/validation.rst | 4 ++-- graphene/validation/tests/test_depth_limit_validator.py | 4 ++-- graphene/validation/tests/test_disable_introspection.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/execution/validation.rst b/docs/execution/validation.rst index ac27ec438..7be4fd664 100644 --- a/docs/execution/validation.rst +++ b/docs/execution/validation.rst @@ -20,7 +20,7 @@ Example Here is how you would implement depth-limiting on your schema. .. code:: python - from graphql import validate + from graphql import validate, parse from graphene import ObjectType, Schema, String from graphene.validation import depth_limit_validator @@ -36,7 +36,7 @@ Here is how you would implement depth-limiting on your schema. validation_errors = validate( schema=schema, - document='THE QUERY', + document_ast=parse('THE QUERY'), rules=( depth_limit_validator( max_depth=20 diff --git a/graphene/validation/tests/test_depth_limit_validator.py b/graphene/validation/tests/test_depth_limit_validator.py index 3eea3a32c..ea62f9999 100644 --- a/graphene/validation/tests/test_depth_limit_validator.py +++ b/graphene/validation/tests/test_depth_limit_validator.py @@ -87,8 +87,8 @@ def callback(query_depths): result = query_depths errors = validate( - schema.graphql_schema, - document, + schema=schema.graphql_schema, + document_ast=document, rules=( depth_limit_validator( max_depth=max_depth, diff --git a/graphene/validation/tests/test_disable_introspection.py b/graphene/validation/tests/test_disable_introspection.py index c13786ed8..b7f0b83fe 100644 --- a/graphene/validation/tests/test_disable_introspection.py +++ b/graphene/validation/tests/test_disable_introspection.py @@ -19,8 +19,8 @@ def run_query(query: str): result = None errors = validate( - schema.graphql_schema, - document, + schema=schema.graphql_schema, + document_ast=document, rules=( disable_introspection(), ), From c68071952da8da3e6ca9d125587e626a8d02d8cd Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Sat, 14 Aug 2021 08:20:46 +0530 Subject: [PATCH 09/23] mention how to implement custom validators --- docs/execution/index.rst | 2 +- docs/execution/queryvalidation.rst | 87 ++++++++++++++++++++++++++++++ docs/execution/validation.rst | 45 ---------------- 3 files changed, 88 insertions(+), 46 deletions(-) create mode 100644 docs/execution/queryvalidation.rst delete mode 100644 docs/execution/validation.rst diff --git a/docs/execution/index.rst b/docs/execution/index.rst index f775cc007..f26259d36 100644 --- a/docs/execution/index.rst +++ b/docs/execution/index.rst @@ -10,4 +10,4 @@ Execution dataloader fileuploading subscriptions - validation + queryvalidation diff --git a/docs/execution/queryvalidation.rst b/docs/execution/queryvalidation.rst new file mode 100644 index 000000000..35f3577e2 --- /dev/null +++ b/docs/execution/queryvalidation.rst @@ -0,0 +1,87 @@ +Query Validation +========== +GraphQL uses query validators to check if Query AST is valid and can be executed. Every GraphQL server implements +standard query validators. For example, there is an validator that tests if queried field exists on queried type, that +makes query fail with "Cannot query field on type" error if it doesn't. + +To help with common use cases, graphene provides a few validation rules out of the box. + + +Depth limit Validator +----------------- +The depth limit validator helps to prevent execution of malicious +queries. It takes in the following arguments. + +- ``max_depth`` is the maximum allowed depth for any operation in a GraphQL document. +- ``ignore`` Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that returns a boolean +- ``callback`` Called each time validation runs. Receives an Object which is a map of the depths for each operation. + +Example +------- + +Here is how you would implement depth-limiting on your schema. + +.. code:: python + from graphql import validate, parse + from graphene import ObjectType, Schema, String + from graphene.validation import depth_limit_validator + + + class MyQuery(ObjectType): + name = String(required=True) + + + schema = Schema(query=MyQuery) + + # Queries which have a depth more than 20 + # will not be executed. + + validation_errors = validate( + schema=schema, + document_ast=parse('THE QUERY'), + rules=( + depth_limit_validator( + max_depth=20 + ), + ) + ) + + +Implementing custom validators +------------------------------ +All custom query validators should extend the `ValidationRule `_ +base class importable from the graphql.validation.rules module. Query validators are visitor classes. They are +instantiated at the time of query validation with one required argument (context: ASTValidationContext). In order to +perform validation, your validator class should define one or more of enter_* and leave_* methods. For possible +enter/leave items as well as details on function documentation, please see contents of the visitor module. To make +validation fail, you should call validator's report_error method with the instance of GraphQLError describing failure +reason. Here is an example query validator that visits field definitions in GraphQL query and fails query validation +if any of those fields are introspection fields: + +.. code:: python + from graphql import GraphQLError + from graphql.language import FieldNode + from graphql.validation import ValidationRule + + + my_blacklist = ( + "disallowed_field", + ) + + + def is_blacklisted_field(field_name: str): + return key.lower() in my_blacklist + + + class BlackListRule(ValidationRule): + def enter_field(self, node: FieldNode, *_args): + field_name = node.name.value + if not is_blacklisted_field(field_name): + return + + self.report_error( + GraphQLError( + f"Cannot query '{field_name}': field is blacklisted.", node, + ) + ) + diff --git a/docs/execution/validation.rst b/docs/execution/validation.rst deleted file mode 100644 index 7be4fd664..000000000 --- a/docs/execution/validation.rst +++ /dev/null @@ -1,45 +0,0 @@ -Validation -========== - -Validation rules help validate a given GraphQL query, before executing it. To help with common use -cases, graphene provides a few validation rules out of the box. - - -Depth limit Validator ------------------ -The depth limit validator helps to prevent execution of malicious -queries. It takes in the following arguments. - -- ``max_depth`` is the maximum allowed depth for any operation in a GraphQL document. -- ``ignore`` Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that returns a boolean -- ``callback`` Called each time validation runs. Receives an Object which is a map of the depths for each operation. - -Example -------- - -Here is how you would implement depth-limiting on your schema. - -.. code:: python - from graphql import validate, parse - from graphene import ObjectType, Schema, String - from graphene.validation import depth_limit_validator - - - class MyQuery(ObjectType): - name = String(required=True) - - - schema = Schema(query=MyQuery) - - # Queries which have a depth more than 20 - # will not be executed. - - validation_errors = validate( - schema=schema, - document_ast=parse('THE QUERY'), - rules=( - depth_limit_validator( - max_depth=20 - ), - ) - ) From ec982ac50b2c79dc956f4e59fde5cf40092af0d8 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Sat, 14 Aug 2021 08:22:04 +0530 Subject: [PATCH 10/23] update docs typo --- docs/execution/queryvalidation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/execution/queryvalidation.rst b/docs/execution/queryvalidation.rst index 35f3577e2..2d58f7ab3 100644 --- a/docs/execution/queryvalidation.rst +++ b/docs/execution/queryvalidation.rst @@ -56,7 +56,7 @@ perform validation, your validator class should define one or more of enter_* an enter/leave items as well as details on function documentation, please see contents of the visitor module. To make validation fail, you should call validator's report_error method with the instance of GraphQLError describing failure reason. Here is an example query validator that visits field definitions in GraphQL query and fails query validation -if any of those fields are introspection fields: +if any of those fields are blacklisted fields: .. code:: python from graphql import GraphQLError From 4e32dac25118e8b043601d61229a1cacfda9cbf6 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Sat, 14 Aug 2021 08:41:24 +0530 Subject: [PATCH 11/23] add tests and docs for disable introspection rule --- docs/execution/queryvalidation.rst | 41 +++++++++++++++++-- graphene/validation/__init__.py | 6 +-- graphene/validation/depth_limit.py | 6 +-- graphene/validation/disable_introspection.py | 23 +++++------ .../tests/test_depth_limit_validator.py | 2 +- .../tests/test_disable_introspection.py | 24 ++++++++--- 6 files changed, 73 insertions(+), 29 deletions(-) diff --git a/docs/execution/queryvalidation.rst b/docs/execution/queryvalidation.rst index 2d58f7ab3..8402b9ea9 100644 --- a/docs/execution/queryvalidation.rst +++ b/docs/execution/queryvalidation.rst @@ -16,7 +16,7 @@ queries. It takes in the following arguments. - ``ignore`` Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that returns a boolean - ``callback`` Called each time validation runs. Receives an Object which is a map of the depths for each operation. -Example +Usage ------- Here is how you would implement depth-limiting on your schema. @@ -33,7 +33,7 @@ Here is how you would implement depth-limiting on your schema. schema = Schema(query=MyQuery) - # Queries which have a depth more than 20 + # queries which have a depth more than 20 # will not be executed. validation_errors = validate( @@ -47,6 +47,39 @@ Here is how you would implement depth-limiting on your schema. ) +Disable Introspection +--------------------- +the disable introspection validation rule ensures that your schema cannot be introspected. +This is a useful security measure in production environments. + +Usage +------- + +Here is how you would disable introspection for your schema. + +.. code:: python + from graphql import validate, parse + from graphene import ObjectType, Schema, String + from graphene.validation import DisableIntrospection + + + class MyQuery(ObjectType): + name = String(required=True) + + + schema = Schema(query=MyQuery) + + # introspection queries will not be executed. + + validation_errors = validate( + schema=schema, + document_ast=parse('THE QUERY'), + rules=( + DisableIntrospection, + ) + ) + + Implementing custom validators ------------------------------ All custom query validators should extend the `ValidationRule `_ @@ -56,7 +89,7 @@ perform validation, your validator class should define one or more of enter_* an enter/leave items as well as details on function documentation, please see contents of the visitor module. To make validation fail, you should call validator's report_error method with the instance of GraphQLError describing failure reason. Here is an example query validator that visits field definitions in GraphQL query and fails query validation -if any of those fields are blacklisted fields: +if any of those fields are blacklisted: .. code:: python from graphql import GraphQLError @@ -70,7 +103,7 @@ if any of those fields are blacklisted fields: def is_blacklisted_field(field_name: str): - return key.lower() in my_blacklist + return field_name.lower() in my_blacklist class BlackListRule(ValidationRule): diff --git a/graphene/validation/__init__.py b/graphene/validation/__init__.py index 03e4605c8..f338e2d0d 100644 --- a/graphene/validation/__init__.py +++ b/graphene/validation/__init__.py @@ -1,8 +1,8 @@ from .depth_limit import depth_limit_validator -from .disable_introspection import disable_introspection +from .disable_introspection import DisableIntrospection __all__ = [ - "depth_limit_validator", - "disable_introspection" + "DisableIntrospection", + "depth_limit_validator" ] diff --git a/graphene/validation/depth_limit.py b/graphene/validation/depth_limit.py index 4136555d7..8363a6c9c 100644 --- a/graphene/validation/depth_limit.py +++ b/graphene/validation/depth_limit.py @@ -116,7 +116,7 @@ def determine_depth( if depth_so_far > max_depth: context.report_error( GraphQLError( - f"'{operation_name}' exceeds maximum operation depth of {max_depth}", + f"'{operation_name}' exceeds maximum operation depth of {max_depth}.", [node], ) ) @@ -172,7 +172,7 @@ def determine_depth( ) ) else: - raise Exception(f"Depth crawler cannot handle: {node.kind}") # pragma: no cover + raise Exception(f"Depth crawler cannot handle: {node.kind}.") # pragma: no cover def is_ignored(node: FieldNode, ignore: Optional[List[IgnoreType]] = None) -> bool: @@ -191,6 +191,6 @@ def is_ignored(node: FieldNode, ignore: Optional[List[IgnoreType]] = None) -> bo if rule(field_name): return True else: - raise ValueError(f"Invalid ignore option: {rule}") + raise ValueError(f"Invalid ignore option: {rule}.") return False diff --git a/graphene/validation/disable_introspection.py b/graphene/validation/disable_introspection.py index eb24be554..4c83050e6 100644 --- a/graphene/validation/disable_introspection.py +++ b/graphene/validation/disable_introspection.py @@ -5,18 +5,15 @@ from ..utils.is_introspection_key import is_introspection_key -def disable_introspection(): - class DisableIntrospection(ValidationRule): - def enter_field(self, node: FieldNode, *_args): - field_name = node.name.value - if not is_introspection_key(field_name): - return +class DisableIntrospection(ValidationRule): + def enter_field(self, node: FieldNode, *_args): + field_name = node.name.value + if not is_introspection_key(field_name): + return - self.report_error( - GraphQLError( - f"Cannot query '{field_name}': introspection is disabled.", - node, - ) + self.report_error( + GraphQLError( + f"Cannot query '{field_name}': introspection is disabled.", + node, ) - - return DisableIntrospection + ) diff --git a/graphene/validation/tests/test_depth_limit_validator.py b/graphene/validation/tests/test_depth_limit_validator.py index ea62f9999..499adbcce 100644 --- a/graphene/validation/tests/test_depth_limit_validator.py +++ b/graphene/validation/tests/test_depth_limit_validator.py @@ -235,7 +235,7 @@ def test_should_catch_very_deep_query(): errors, result = run_query(query, 4) assert len(errors) == 1 - assert errors[0].message == "'anonymous' exceeds maximum operation depth of 4" + assert errors[0].message == "'anonymous' exceeds maximum operation depth of 4." def test_should_ignore_field(): diff --git a/graphene/validation/tests/test_disable_introspection.py b/graphene/validation/tests/test_disable_introspection.py index b7f0b83fe..060199001 100644 --- a/graphene/validation/tests/test_disable_introspection.py +++ b/graphene/validation/tests/test_disable_introspection.py @@ -1,7 +1,7 @@ from graphql import parse, validate from ...types import Schema, ObjectType, String -from ..disable_introspection import disable_introspection +from ..disable_introspection import DisableIntrospection class Query(ObjectType): @@ -9,6 +9,10 @@ class Query(ObjectType): required=True ) + @staticmethod + def resolve_name(root, info): + return "Hello world!" + schema = Schema(query=Query) @@ -16,14 +20,24 @@ class Query(ObjectType): def run_query(query: str): document = parse(query) - result = None - errors = validate( schema=schema.graphql_schema, document_ast=document, rules=( - disable_introspection(), + DisableIntrospection, ), ) - return errors, result + return errors + + +def test_disallows_introspection_queries(): + errors = run_query("{ __schema { queryType { name } } }") + + assert len(errors) == 1 + assert errors[0].message == "Cannot query '__schema': introspection is disabled." + + +def test_allows_non_introspection_queries(): + errors = run_query("{ name }") + assert len(errors) == 0 From b4be4a686bd2d5279433dc77346b279f13d3f1e3 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Thu, 19 Aug 2021 10:59:58 +0530 Subject: [PATCH 12/23] add notice to failing tests --- graphene/types/tests/test_schema.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py index 54c48b4f2..f84d2e204 100644 --- a/graphene/types/tests/test_schema.py +++ b/graphene/types/tests/test_schema.py @@ -175,6 +175,11 @@ def test_nested_graphql_error(self, schema): ], ) def test_unexpected_error(self, field, exception, schema): + # FIXME: tests are failing currently because no exception + # is being raised below. Instead, the errors are being propagated + # to the `errors` array of the response. If this is intended + # behaviour, we need to check if the error exists in the `errors` + # array rather than checking if an exception is raised. with raises(exception): # no result, but the exception should be propagated schema.execute( From 467b1f8e8d30d59a1422bbfe2805e20145f0a7fd Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Thu, 19 Aug 2021 12:03:27 +0530 Subject: [PATCH 13/23] add workflow: tests --- .github/{ => workflows}/stale.yml | 0 .github/workflows/tests.yml | 85 +++++++++++++++++++ ...mmit-config.yaml => .pre-commit-config.yml | 0 .travis.yml | 42 --------- 4 files changed, 85 insertions(+), 42 deletions(-) rename .github/{ => workflows}/stale.yml (100%) create mode 100644 .github/workflows/tests.yml rename .pre-commit-config.yaml => .pre-commit-config.yml (100%) delete mode 100644 .travis.yml diff --git a/.github/stale.yml b/.github/workflows/stale.yml similarity index 100% rename from .github/stale.yml rename to .github/workflows/stale.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..cf8b87190 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,85 @@ +name: 📄 Tests +on: + push: + branches: + - master + - '*.x' + paths-ignore: + - 'docs/**' + - '*.md' + - '*.rst' + pull_request: + branches: + - master + - '*.x' + paths-ignore: + - 'docs/**' + - '*.md' + - '*.rst' +jobs: + tests: + # runs the test suite + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38} + - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37} + - {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + + - name: update pip + run: | + pip install -U wheel + pip install -U setuptools + python -m pip install -U pip + + - name: get pip cache dir + id: pip-cache + run: echo "::set-output name=dir::$(pip cache dir)" + + - name: cache pip + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }} + + - run: pip install tox + - run: tox -e ${{ matrix.tox }} + + coveralls: + # check coverage increase/decrease + needs: tests + runs-on: ${{ matrix.os }} + steps: + - name: Coveralls Finished + uses: AndreMiras/coveralls-python-action@develop + with: + parallel-finished: true + + deploy: + # builds and publishes to PyPi + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yml similarity index 100% rename from .pre-commit-config.yaml rename to .pre-commit-config.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e1e551198..000000000 --- a/.travis.yml +++ /dev/null @@ -1,42 +0,0 @@ -language: python -dist: xenial - -python: - - "3.6" - - "3.7" - - "3.8" - -install: - - pip install tox tox-travis -script: tox -after_success: - - pip install coveralls - - coveralls -cache: - directories: - - $HOME/.cache/pip - - $HOME/.cache/pre-commit - -stages: - - test - - name: deploy - if: tag IS present - -jobs: - fast_finish: true - include: - - env: TOXENV=pre-commit - python: 3.7 - - env: TOXENV=mypy - python: 3.7 - - stage: deploy - python: 3.7 - after_success: true - deploy: - provider: pypi - user: syrusakbary - on: - tags: true - password: - secure: LHOp9DvYR+70vj4YVY8+JRNCKUOfYZREEUY3+4lMUpY7Zy5QwDfgEMXG64ybREH9dFldpUqVXRj53eeU3spfudSfh8NHkgqW7qihez2AhSnRc4dK6ooNfB+kLcSoJ4nUFGxdYImABc4V1hJvflGaUkTwDNYVxJF938bPaO797IvSbuI86llwqkvuK2Vegv9q/fy9sVGaF9VZIs4JgXwR5AyDR7FBArl+S84vWww4vTFD33hoE88VR4QvFY3/71BwRtQrnCMm7AOm31P9u29yi3bpzQpiOR2rHsgrsYdm597QzFKVxYwsmf9uAx2bpbSPy2WibunLePIvOFwm8xcfwnz4/J4ONBc5PSFmUytTWpzEnxb0bfUNLuYloIS24V6OZ8BfAhiYZ1AwySeJCQDM4Vk1V8IF6trTtyx5EW/uV9jsHCZ3LFsAD7UnFRTosIgN3SAK3ZWCEk5oF2IvjecsolEfkRXB3q9EjMkkuXRUeFDH2lWJLgNE27BzY6myvZVzPmfwZUsPBlPD/6w+WLSp97Rjgr9zS3T1d4ddqFM4ZYu04f2i7a/UUQqG+itzzuX5DWLPvzuNt37JB45mB9IsvxPyXZ6SkAcLl48NGyKok1f3vQnvphkfkl4lni29woKhaau8xlsuEDrcwOoeAsVcZXiItg+l+z2SlIwM0A06EvQ= - distributions: "sdist bdist_wheel" From c0ddbbfaf4fd4a777834dec7662f8ce9d85ffb50 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Thu, 19 Aug 2021 12:13:46 +0530 Subject: [PATCH 14/23] update workflow matrix --- .github/workflows/tests.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cf8b87190..b7cb3fdb4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -57,6 +57,10 @@ jobs: # check coverage increase/decrease needs: tests runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - { os: ubuntu-latest } steps: - name: Coveralls Finished uses: AndreMiras/coveralls-python-action@develop @@ -66,12 +70,16 @@ jobs: deploy: # builds and publishes to PyPi runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - { python: '3.7', os: ubuntu-latest } steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: - python-version: '3.7' + python-version: ${{ matrix.python }} - name: Install dependencies run: | python -m pip install --upgrade pip From 8ae436915575f2efefdef2e289cd04d667c9c6a8 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Thu, 19 Aug 2021 12:16:13 +0530 Subject: [PATCH 15/23] remove build matrix wherever not needed --- .github/workflows/tests.yml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b7cb3fdb4..39f15ef40 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,11 +56,7 @@ jobs: coveralls: # check coverage increase/decrease needs: tests - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - { os: ubuntu-latest } + runs-on: "ubuntu-latest" steps: - name: Coveralls Finished uses: AndreMiras/coveralls-python-action@develop @@ -69,17 +65,13 @@ jobs: deploy: # builds and publishes to PyPi - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - { python: '3.7', os: ubuntu-latest } + runs-on: "ubuntu-latest" steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: - python-version: ${{ matrix.python }} + python-version: "3.7" - name: Install dependencies run: | python -m pip install --upgrade pip From 0e4c14b0767627c504cc0f0adee9a21824fd05a6 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+aryan340@users.noreply.github.com> Date: Thu, 19 Aug 2021 15:00:09 +0530 Subject: [PATCH 16/23] update workflow: tests --- .github/workflows/tests.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 39f15ef40..6de43f373 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,7 +44,7 @@ jobs: id: pip-cache run: echo "::set-output name=dir::$(pip cache dir)" - - name: cache pip + - name: cache pip dependencies uses: actions/cache@v2 with: path: ${{ steps.pip-cache.outputs.dir }} @@ -53,25 +53,23 @@ jobs: - run: pip install tox - run: tox -e ${{ matrix.tox }} - coveralls: + coveralls_finish: # check coverage increase/decrease needs: tests - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: AndreMiras/coveralls-python-action@develop - with: - parallel-finished: true deploy: # builds and publishes to PyPi - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: - python-version: "3.7" + python-version: '3.7' - name: Install dependencies run: | python -m pip install --upgrade pip From 7d890bf91521a3e7905e95f442a4e934a68603fb Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 19 Aug 2021 14:02:45 -0500 Subject: [PATCH 17/23] Update graphene/validation/disable_introspection.py --- graphene/validation/disable_introspection.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/graphene/validation/disable_introspection.py b/graphene/validation/disable_introspection.py index 4c83050e6..be25a2871 100644 --- a/graphene/validation/disable_introspection.py +++ b/graphene/validation/disable_introspection.py @@ -8,12 +8,10 @@ class DisableIntrospection(ValidationRule): def enter_field(self, node: FieldNode, *_args): field_name = node.name.value - if not is_introspection_key(field_name): - return - - self.report_error( - GraphQLError( - f"Cannot query '{field_name}': introspection is disabled.", - node, + if is_introspection_key(field_name): + self.report_error( + GraphQLError( + f"Cannot query '{field_name}': introspection is disabled.", + node, + ) ) - ) From 946c2a3807d8970deee4f51eed07144349f1dde3 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Fri, 20 Aug 2021 15:58:43 +0530 Subject: [PATCH 18/23] Update schema.py --- graphene/types/schema.py | 236 --------------------------------------- 1 file changed, 236 deletions(-) diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 995323542..9d3c8be50 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -391,239 +391,3 @@ def resolve_type(self, resolve_type_func, type_name, root, info, _type): return graphql_type return type_ - - -class UnforgivingExecutionContext(ExecutionContext): - """An execution context which doesn't swallow exceptions. - - The only difference between this execution context and the one it inherits from is - that ``except Exception`` is commented out within ``resolve_field_value_or_error``. - By removing that exception handling, only ``GraphQLError``'s are caught. - """ - - def resolve_field_value_or_error( - self, field_def, field_nodes, resolve_fn, source, info - ): - """Resolve field to a value or an error. - - Isolates the "ReturnOrAbrupt" behavior to not de-opt the resolve_field() - method. Returns the result of resolveFn or the abrupt-return Error object. - - For internal use only. - """ - try: - # Build a dictionary of arguments from the field.arguments AST, using the - # variables scope to fulfill any variable references. - args = get_argument_values(field_def, field_nodes[0], self.variable_values) - - # Note that contrary to the JavaScript implementation, we pass the context - # value as part of the resolve info. - result = resolve_fn(source, info, **args) - if self.is_awaitable(result): - # noinspection PyShadowingNames - async def await_result(): - try: - return await result - except GraphQLError as error: - return error - # except Exception as error: - # return GraphQLError(str(error), original_error=error) - - # Yes, this is commented out code. It's been intentionally - # _not_ removed to show what has changed from the original - # implementation. - - return await_result() - return result - except GraphQLError as error: - return error - # except Exception as error: - # return GraphQLError(str(error), original_error=error) - - # Yes, this is commented out code. It's been intentionally _not_ - # removed to show what has changed from the original implementation. - - def complete_value_catching_error( - self, return_type, field_nodes, info, path, result - ): - """Complete a value while catching an error. - - This is a small wrapper around completeValue which detects and logs errors in - the execution context. - """ - try: - if self.is_awaitable(result): - - async def await_result(): - value = self.complete_value( - return_type, field_nodes, info, path, await result - ) - if self.is_awaitable(value): - return await value - return value - - completed = await_result() - else: - completed = self.complete_value( - return_type, field_nodes, info, path, result - ) - if self.is_awaitable(completed): - # noinspection PyShadowingNames - async def await_completed(): - try: - return await completed - - # CHANGE WAS MADE HERE - # ``GraphQLError`` was swapped in for ``except Exception`` - except GraphQLError as error: - self.handle_field_error(error, field_nodes, path, return_type) - - return await_completed() - return completed - - # CHANGE WAS MADE HERE - # ``GraphQLError`` was swapped in for ``except Exception`` - except GraphQLError as error: - self.handle_field_error(error, field_nodes, path, return_type) - return None - - -class Schema: - """Schema Definition. - - A Graphene Schema can execute operations (query, mutation, subscription) against the defined - types. For advanced purposes, the schema can be used to lookup type definitions and answer - questions about the types through introspection. - - Args: - query (Type[ObjectType]): Root query *ObjectType*. Describes entry point for fields to *read* - data in your Schema. - mutation (Optional[Type[ObjectType]]): Root mutation *ObjectType*. Describes entry point for - fields to *create, update or delete* data in your API. - subscription (Optional[Type[ObjectType]]): Root subscription *ObjectType*. Describes entry point - for fields to receive continuous updates. - types (Optional[List[Type[ObjectType]]]): List of any types to include in schema that - may not be introspected through root types. - directives (List[GraphQLDirective], optional): List of custom directives to include in the - GraphQL schema. Defaults to only include directives defined by GraphQL spec (@include - and @skip) [GraphQLIncludeDirective, GraphQLSkipDirective]. - auto_camelcase (bool): Fieldnames will be transformed in Schema's TypeMap from snake_case - to camelCase (preferred by GraphQL standard). Default True. - """ - - def __init__( - self, - query=None, - mutation=None, - subscription=None, - types=None, - directives=None, - auto_camelcase=True, - ): - self.query = query - self.mutation = mutation - self.subscription = subscription - type_map = TypeMap( - query, mutation, subscription, types, auto_camelcase=auto_camelcase - ) - self.graphql_schema = GraphQLSchema( - type_map.query, - type_map.mutation, - type_map.subscription, - type_map.types, - directives, - ) - - def __str__(self): - return print_schema(self.graphql_schema) - - def __getattr__(self, type_name): - """ - This function let the developer select a type in a given schema - by accessing its attrs. - - Example: using schema.Query for accessing the "Query" type in the Schema - """ - _type = self.graphql_schema.get_type(type_name) - if _type is None: - raise AttributeError(f'Type "{type_name}" not found in the Schema') - if isinstance(_type, GrapheneGraphQLType): - return _type.graphene_type - return _type - - def lazy(self, _type): - return lambda: self.get_type(_type) - - def execute(self, *args, **kwargs): - """Execute a GraphQL query on the schema. - - Use the `graphql_sync` function from `graphql-core` to provide the result - for a query string. Most of the time this method will be called by one of the Graphene - :ref:`Integrations` via a web request. - - Args: - request_string (str or Document): GraphQL request (query, mutation or subscription) - as string or parsed AST form from `graphql-core`. - root_value (Any, optional): Value to use as the parent value object when resolving - root types. - context_value (Any, optional): Value to be made available to all resolvers via - `info.context`. Can be used to share authorization, dataloaders or other - information needed to resolve an operation. - variable_values (dict, optional): If variables are used in the request string, they can - be provided in dictionary form mapping the variable name to the variable value. - operation_name (str, optional): If multiple operations are provided in the - request_string, an operation name must be provided for the result to be provided. - middleware (List[SupportsGraphQLMiddleware]): Supply request level middleware as - defined in `graphql-core`. - execution_context_class (ExecutionContext, optional): The execution context class - to use when resolving queries and mutations. - - Returns: - :obj:`ExecutionResult` containing any data and errors for the operation. - """ - kwargs = normalize_execute_kwargs(kwargs) - return graphql_sync(self.graphql_schema, *args, **kwargs) - - async def execute_async(self, *args, **kwargs): - """Execute a GraphQL query on the schema asynchronously. - - Same as `execute`, but uses `graphql` instead of `graphql_sync`. - """ - kwargs = normalize_execute_kwargs(kwargs) - return await graphql(self.graphql_schema, *args, **kwargs) - - async def subscribe(self, query, *args, **kwargs): - """Execute a GraphQL subscription on the schema asynchronously.""" - # Do parsing - try: - document = parse(query) - except GraphQLError as error: - return ExecutionResult(data=None, errors=[error]) - - # Do validation - validation_errors = validate(self.graphql_schema, document) - if validation_errors: - return ExecutionResult(data=None, errors=validation_errors) - - # Execute the query - kwargs = normalize_execute_kwargs(kwargs) - return await subscribe(self.graphql_schema, document, *args, **kwargs) - - def introspect(self): - introspection = self.execute(introspection_query) - if introspection.errors: - raise introspection.errors[0] - return introspection.data - - -def normalize_execute_kwargs(kwargs): - """Replace alias names in keyword arguments for graphql()""" - if "root" in kwargs and "root_value" not in kwargs: - kwargs["root_value"] = kwargs.pop("root") - if "context" in kwargs and "context_value" not in kwargs: - kwargs["context_value"] = kwargs.pop("context") - if "variables" in kwargs and "variable_values" not in kwargs: - kwargs["variable_values"] = kwargs.pop("variables") - if "operation" in kwargs and "operation_name" not in kwargs: - kwargs["operation_name"] = kwargs.pop("operation") - return kwargs From 18cd3451f9715f3db900a64fd288b00b6706c003 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Fri, 20 Aug 2021 15:59:38 +0530 Subject: [PATCH 19/23] Update test_schema.py --- graphene/types/tests/test_schema.py | 119 +--------------------------- 1 file changed, 1 insertion(+), 118 deletions(-) diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py index f84d2e204..9cdbde3bc 100644 --- a/graphene/types/tests/test_schema.py +++ b/graphene/types/tests/test_schema.py @@ -7,7 +7,7 @@ from ..field import Field from ..objecttype import ObjectType from ..scalars import String -from ..schema import Schema, UnforgivingExecutionContext +from ..schema import Schema class MyOtherType(ObjectType): @@ -69,120 +69,3 @@ def test_schema_requires_query_type(): assert len(result.errors) == 1 error = result.errors[0] assert error.message == "Query root type must be provided." - - -class TestUnforgivingExecutionContext: - @fixture - def schema(self): - class ErrorFieldsMixin: - sanity_field = String() - expected_error_field = String() - unexpected_value_error_field = String() - unexpected_type_error_field = String() - unexpected_attribute_error_field = String() - unexpected_key_error_field = String() - - @staticmethod - def resolve_sanity_field(obj, info): - return "not an error" - - @staticmethod - def resolve_expected_error_field(obj, info): - raise GraphQLError("expected error") - - @staticmethod - def resolve_unexpected_value_error_field(obj, info): - raise ValueError("unexpected error") - - @staticmethod - def resolve_unexpected_type_error_field(obj, info): - raise TypeError("unexpected error") - - @staticmethod - def resolve_unexpected_attribute_error_field(obj, info): - raise AttributeError("unexpected error") - - @staticmethod - def resolve_unexpected_key_error_field(obj, info): - return {}["fails"] - - class NestedObject(ErrorFieldsMixin, ObjectType): - pass - - class MyQuery(ErrorFieldsMixin, ObjectType): - nested_object = Field(NestedObject) - nested_object_error = Field(NestedObject) - - @staticmethod - def resolve_nested_object(obj, info): - return object() - - @staticmethod - def resolve_nested_object_error(obj, info): - raise TypeError() - - schema = Schema(query=MyQuery) - return schema - - def test_sanity_check(self, schema): - # this should pass with no errors (sanity check) - result = schema.execute( - "query { sanityField }", - execution_context_class=UnforgivingExecutionContext, - ) - assert not result.errors - assert result.data == {"sanityField": "not an error"} - - def test_nested_sanity_check(self, schema): - # this should pass with no errors (sanity check) - result = schema.execute( - r"query { nestedObject { sanityField } }", - execution_context_class=UnforgivingExecutionContext, - ) - assert not result.errors - assert result.data == {"nestedObject": {"sanityField": "not an error"}} - - def test_graphql_error(self, schema): - result = schema.execute( - "query { expectedErrorField }", - execution_context_class=UnforgivingExecutionContext, - ) - assert len(result.errors) == 1 - assert result.errors[0].message == "expected error" - assert result.data == {"expectedErrorField": None} - - def test_nested_graphql_error(self, schema): - result = schema.execute( - r"query { nestedObject { expectedErrorField } }", - execution_context_class=UnforgivingExecutionContext, - ) - assert len(result.errors) == 1 - assert result.errors[0].message == "expected error" - assert result.data == {"nestedObject": {"expectedErrorField": None}} - - @mark.parametrize( - "field,exception", - [ - ("unexpectedValueErrorField", ValueError), - ("unexpectedTypeErrorField", TypeError), - ("unexpectedAttributeErrorField", AttributeError), - ("unexpectedKeyErrorField", KeyError), - ("nestedObject { unexpectedValueErrorField }", ValueError), - ("nestedObject { unexpectedTypeErrorField }", TypeError), - ("nestedObject { unexpectedAttributeErrorField }", AttributeError), - ("nestedObject { unexpectedKeyErrorField }", KeyError), - ("nestedObjectError { __typename }", TypeError), - ], - ) - def test_unexpected_error(self, field, exception, schema): - # FIXME: tests are failing currently because no exception - # is being raised below. Instead, the errors are being propagated - # to the `errors` array of the response. If this is intended - # behaviour, we need to check if the error exists in the `errors` - # array rather than checking if an exception is raised. - with raises(exception): - # no result, but the exception should be propagated - schema.execute( - f"query {{ {field} }}", - execution_context_class=UnforgivingExecutionContext, - ) From ea4e6d65e9db41c69da9a23ecaaceaecb084054a Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Fri, 20 Aug 2021 16:08:58 +0530 Subject: [PATCH 20/23] Update schema.py --- graphene/types/schema.py | 134 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 9d3c8be50..1ff0bff4f 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -391,3 +391,137 @@ def resolve_type(self, resolve_type_func, type_name, root, info, _type): return graphql_type return type_ + + +class Schema: + """Schema Definition. + A Graphene Schema can execute operations (query, mutation, subscription) against the defined + types. For advanced purposes, the schema can be used to lookup type definitions and answer + questions about the types through introspection. + Args: + query (Type[ObjectType]): Root query *ObjectType*. Describes entry point for fields to *read* + data in your Schema. + mutation (Optional[Type[ObjectType]]): Root mutation *ObjectType*. Describes entry point for + fields to *create, update or delete* data in your API. + subscription (Optional[Type[ObjectType]]): Root subscription *ObjectType*. Describes entry point + for fields to receive continuous updates. + types (Optional[List[Type[ObjectType]]]): List of any types to include in schema that + may not be introspected through root types. + directives (List[GraphQLDirective], optional): List of custom directives to include in the + GraphQL schema. Defaults to only include directives defined by GraphQL spec (@include + and @skip) [GraphQLIncludeDirective, GraphQLSkipDirective]. + auto_camelcase (bool): Fieldnames will be transformed in Schema's TypeMap from snake_case + to camelCase (preferred by GraphQL standard). Default True. + """ + + def __init__( + self, + query=None, + mutation=None, + subscription=None, + types=None, + directives=None, + auto_camelcase=True, + ): + self.query = query + self.mutation = mutation + self.subscription = subscription + type_map = TypeMap( + query, mutation, subscription, types, auto_camelcase=auto_camelcase + ) + self.graphql_schema = GraphQLSchema( + type_map.query, + type_map.mutation, + type_map.subscription, + type_map.types, + directives, + ) + + def __str__(self): + return print_schema(self.graphql_schema) + + def __getattr__(self, type_name): + """ + This function let the developer select a type in a given schema + by accessing its attrs. + Example: using schema.Query for accessing the "Query" type in the Schema + """ + _type = self.graphql_schema.get_type(type_name) + if _type is None: + raise AttributeError(f'Type "{type_name}" not found in the Schema') + if isinstance(_type, GrapheneGraphQLType): + return _type.graphene_type + return _type + + def lazy(self, _type): + return lambda: self.get_type(_type) + + def execute(self, *args, **kwargs): + """Execute a GraphQL query on the schema. + Use the `graphql_sync` function from `graphql-core` to provide the result + for a query string. Most of the time this method will be called by one of the Graphene + :ref:`Integrations` via a web request. + Args: + request_string (str or Document): GraphQL request (query, mutation or subscription) + as string or parsed AST form from `graphql-core`. + root_value (Any, optional): Value to use as the parent value object when resolving + root types. + context_value (Any, optional): Value to be made available to all resolvers via + `info.context`. Can be used to share authorization, dataloaders or other + information needed to resolve an operation. + variable_values (dict, optional): If variables are used in the request string, they can + be provided in dictionary form mapping the variable name to the variable value. + operation_name (str, optional): If multiple operations are provided in the + request_string, an operation name must be provided for the result to be provided. + middleware (List[SupportsGraphQLMiddleware]): Supply request level middleware as + defined in `graphql-core`. + execution_context_class (ExecutionContext, optional): The execution context class + to use when resolving queries and mutations. + Returns: + :obj:`ExecutionResult` containing any data and errors for the operation. + """ + kwargs = normalize_execute_kwargs(kwargs) + return graphql_sync(self.graphql_schema, *args, **kwargs) + + async def execute_async(self, *args, **kwargs): + """Execute a GraphQL query on the schema asynchronously. + Same as `execute`, but uses `graphql` instead of `graphql_sync`. + """ + kwargs = normalize_execute_kwargs(kwargs) + return await graphql(self.graphql_schema, *args, **kwargs) + + async def subscribe(self, query, *args, **kwargs): + """Execute a GraphQL subscription on the schema asynchronously.""" + # Do parsing + try: + document = parse(query) + except GraphQLError as error: + return ExecutionResult(data=None, errors=[error]) + + # Do validation + validation_errors = validate(self.graphql_schema, document) + if validation_errors: + return ExecutionResult(data=None, errors=validation_errors) + + # Execute the query + kwargs = normalize_execute_kwargs(kwargs) + return await subscribe(self.graphql_schema, document, *args, **kwargs) + + def introspect(self): + introspection = self.execute(introspection_query) + if introspection.errors: + raise introspection.errors[0] + return introspection.data + + +def normalize_execute_kwargs(kwargs): + """Replace alias names in keyword arguments for graphql()""" + if "root" in kwargs and "root_value" not in kwargs: + kwargs["root_value"] = kwargs.pop("root") + if "context" in kwargs and "context_value" not in kwargs: + kwargs["context_value"] = kwargs.pop("context") + if "variables" in kwargs and "variable_values" not in kwargs: + kwargs["variable_values"] = kwargs.pop("variables") + if "operation" in kwargs and "operation_name" not in kwargs: + kwargs["operation_name"] = kwargs.pop("operation") + return kwargs From 57a4394bf3b149a16e36259a8b26cbe5aadc6970 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Fri, 20 Aug 2021 20:56:19 +0530 Subject: [PATCH 21/23] Update depth_limit.py --- graphene/validation/depth_limit.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/graphene/validation/depth_limit.py b/graphene/validation/depth_limit.py index 8363a6c9c..0a95aeaef 100644 --- a/graphene/validation/depth_limit.py +++ b/graphene/validation/depth_limit.py @@ -25,7 +25,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import re +try: + from re import Pattern +except ImportError: + # backwards compatibility for v3.6 + from typing import Pattern + from typing import Callable, Dict, List, Optional, Union from graphql import GraphQLError @@ -43,7 +48,7 @@ from ..utils.is_introspection_key import is_introspection_key -IgnoreType = Union[Callable[[str], bool], re.Pattern, str] +IgnoreType = Union[Callable[[str], bool], Pattern, str] def depth_limit_validator( From 98980b53f6032c186d94982ebf4d87b4a3bf5f80 Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Fri, 20 Aug 2021 21:04:22 +0530 Subject: [PATCH 22/23] Update depth_limit.py --- graphene/validation/depth_limit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphene/validation/depth_limit.py b/graphene/validation/depth_limit.py index 0a95aeaef..47a044034 100644 --- a/graphene/validation/depth_limit.py +++ b/graphene/validation/depth_limit.py @@ -31,6 +31,7 @@ # backwards compatibility for v3.6 from typing import Pattern +import re from typing import Callable, Dict, List, Optional, Union from graphql import GraphQLError From 74a6565ea3f77f68758b099291fde9544d10d03f Mon Sep 17 00:00:00 2001 From: Aryan Iyappan <69184573+codebyaryan@users.noreply.github.com> Date: Fri, 20 Aug 2021 21:07:57 +0530 Subject: [PATCH 23/23] Update depth_limit.py --- graphene/validation/depth_limit.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphene/validation/depth_limit.py b/graphene/validation/depth_limit.py index 47a044034..c72b78d02 100644 --- a/graphene/validation/depth_limit.py +++ b/graphene/validation/depth_limit.py @@ -31,7 +31,6 @@ # backwards compatibility for v3.6 from typing import Pattern -import re from typing import Callable, Dict, List, Optional, Union from graphql import GraphQLError @@ -190,7 +189,7 @@ def is_ignored(node: FieldNode, ignore: Optional[List[IgnoreType]] = None) -> bo if isinstance(rule, str): if field_name == rule: return True - elif isinstance(rule, re.Pattern): + elif isinstance(rule, Pattern): if rule.match(field_name): return True elif callable(rule):