mirror of
https://github.com/graphql-python/graphene.git
synced 2024-11-26 11:33:55 +03:00
add disable introspection
This commit is contained in:
parent
5977b1648c
commit
a784ef15e5
|
@ -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.
|
||||||
|
|
6
graphene/utils/is_introspection_key.py
Normal file
6
graphene/utils/is_introspection_key.py
Normal 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 GraphQL’s introspection system.
|
||||||
|
return str(node.name.value).startswith("__")
|
8
graphene/validation/__init__.py
Normal file
8
graphene/validation/__init__.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from .depth_limit import depth_limit_validator
|
||||||
|
from .disable_introspection import disable_introspection
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"depth_limit_validator",
|
||||||
|
"disable_introspection"
|
||||||
|
]
|
|
@ -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 GraphQL’s introspection system.
|
|
||||||
should_ignore = str(node.name.value).startswith("__") or is_ignored(
|
|
||||||
node, ignore
|
node, ignore
|
||||||
)
|
)
|
||||||
|
|
22
graphene/validation/disable_introspection.py
Normal file
22
graphene/validation/disable_introspection.py
Normal 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
|
33
graphene/validation/tests/test_disable_introspection.py
Normal file
33
graphene/validation/tests/test_disable_introspection.py
Normal 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
|
|
@ -1,6 +0,0 @@
|
||||||
from .depth_limit_validator import depth_limit_validator
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"depth_limit_validator"
|
|
||||||
]
|
|
|
@ -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],
|
|
||||||
)
|
|
Loading…
Reference in New Issue
Block a user