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

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 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 GraphQLs 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
)

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