add disable introspection

This commit is contained in:
Aryan Iyappan 2021-08-13 20:24:53 +05:30
parent 5977b1648c
commit a784ef15e5
9 changed files with 74 additions and 292 deletions

View File

@ -20,7 +20,7 @@ Example
Here is how you would implement depth-limiting on your schema. Here is how you would implement depth-limiting on your schema.
.. code:: python .. code:: python
from graphene.validators import depth_limit_validator from graphene.validation import depth_limit_validator
# The following schema doesn't execute queries # The following schema doesn't execute queries
# which have a depth more than 20. # which have a depth more than 20.

View File

@ -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 GraphQLs introspection system.
return str(node.name.value).startswith("__")

View File

@ -0,0 +1,8 @@
from .depth_limit import depth_limit_validator
from .disable_introspection import disable_introspection
__all__ = [
"depth_limit_validator",
"disable_introspection"
]

View File

@ -29,6 +29,7 @@ import re
from typing import Callable, Dict, List, Optional, Union from typing import Callable, Dict, List, Optional, Union
from graphql import GraphQLError from graphql import GraphQLError
from graphql.validation import ValidationContext, ValidationRule
from graphql.language import ( from graphql.language import (
DefinitionNode, DefinitionNode,
FieldNode, FieldNode,
@ -38,7 +39,8 @@ from graphql.language import (
Node, Node,
OperationDefinitionNode, OperationDefinitionNode,
) )
from graphql.validation import ValidationContext, ValidationRule
from ..utils.is_introspection_key import is_introspection_key
IgnoreType = Union[Callable[[str], bool], re.Pattern, str] IgnoreType = Union[Callable[[str], bool], re.Pattern, str]
@ -121,11 +123,7 @@ def determine_depth(
return depth_so_far return depth_so_far
if isinstance(node, FieldNode): if isinstance(node, FieldNode):
# from: https://spec.graphql.org/June2018/#sec-Schema should_ignore = is_introspection_key(node.name.value) or is_ignored(
# > All types and directives defined within a schema must not have a name which
# > begins with "__" (two underscores), as this is used exclusively
# > by GraphQLs introspection system.
should_ignore = str(node.name.value).startswith("__") or is_ignored(
node, ignore node, ignore
) )

View File

@ -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

View File

@ -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

View File

@ -1,6 +0,0 @@
from .depth_limit_validator import depth_limit_validator
__all__ = [
"depth_limit_validator"
]

View File

@ -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],
)