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] 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 a37c80ab..92b8ecd2 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 00000000..68951940 --- /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 00000000..03e4605c --- /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 d2589700..4136555d 100644 --- a/graphene/validators/depth_limit_validator.py +++ b/graphene/validation/depth_limit.py @@ -29,6 +29,7 @@ import re 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 @@ from graphql.language import ( 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 00000000..eb24be55 --- /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 00000000..4d1faa7d --- /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 8bd8d884..00000000 --- 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 ea50c8d4..00000000 --- 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], - )