Skip to content

Commit efc0353

Browse files
authored
Merge pull request #1357 from codebyaryan/master
add support for query validation
2 parents fce45ef + 74a6565 commit efc0353

15 files changed

+759
-257
lines changed
File renamed without changes.

.github/workflows/tests.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: 📄 Tests
2+
on:
3+
push:
4+
branches:
5+
- master
6+
- '*.x'
7+
paths-ignore:
8+
- 'docs/**'
9+
- '*.md'
10+
- '*.rst'
11+
pull_request:
12+
branches:
13+
- master
14+
- '*.x'
15+
paths-ignore:
16+
- 'docs/**'
17+
- '*.md'
18+
- '*.rst'
19+
jobs:
20+
tests:
21+
# runs the test suite
22+
name: ${{ matrix.name }}
23+
runs-on: ${{ matrix.os }}
24+
strategy:
25+
fail-fast: false
26+
matrix:
27+
include:
28+
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
29+
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
30+
- {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36}
31+
steps:
32+
- uses: actions/checkout@v2
33+
- uses: actions/setup-python@v2
34+
with:
35+
python-version: ${{ matrix.python }}
36+
37+
- name: update pip
38+
run: |
39+
pip install -U wheel
40+
pip install -U setuptools
41+
python -m pip install -U pip
42+
43+
- name: get pip cache dir
44+
id: pip-cache
45+
run: echo "::set-output name=dir::$(pip cache dir)"
46+
47+
- name: cache pip dependencies
48+
uses: actions/cache@v2
49+
with:
50+
path: ${{ steps.pip-cache.outputs.dir }}
51+
key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }}
52+
53+
- run: pip install tox
54+
- run: tox -e ${{ matrix.tox }}
55+
56+
coveralls_finish:
57+
# check coverage increase/decrease
58+
needs: tests
59+
runs-on: ubuntu-latest
60+
steps:
61+
- name: Coveralls Finished
62+
uses: AndreMiras/coveralls-python-action@develop
63+
64+
deploy:
65+
# builds and publishes to PyPi
66+
runs-on: ubuntu-latest
67+
steps:
68+
- uses: actions/checkout@v2
69+
- name: Set up Python
70+
uses: actions/setup-python@v2
71+
with:
72+
python-version: '3.7'
73+
- name: Install dependencies
74+
run: |
75+
python -m pip install --upgrade pip
76+
pip install build
77+
- name: Build package
78+
run: python -m build
79+
- name: Publish package
80+
uses: pypa/gh-action-pypi-publish@release/v1
81+
with:
82+
user: __token__
83+
password: ${{ secrets.PYPI_API_TOKEN }}
File renamed without changes.

.travis.yml

Lines changed: 0 additions & 42 deletions
This file was deleted.

docs/execution/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ Execution
1010
dataloader
1111
fileuploading
1212
subscriptions
13+
queryvalidation

docs/execution/queryvalidation.rst

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
Query Validation
2+
==========
3+
GraphQL uses query validators to check if Query AST is valid and can be executed. Every GraphQL server implements
4+
standard query validators. For example, there is an validator that tests if queried field exists on queried type, that
5+
makes query fail with "Cannot query field on type" error if it doesn't.
6+
7+
To help with common use cases, graphene provides a few validation rules out of the box.
8+
9+
10+
Depth limit Validator
11+
-----------------
12+
The depth limit validator helps to prevent execution of malicious
13+
queries. It takes in the following arguments.
14+
15+
- ``max_depth`` is the maximum allowed depth for any operation in a GraphQL document.
16+
- ``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
17+
- ``callback`` Called each time validation runs. Receives an Object which is a map of the depths for each operation.
18+
19+
Usage
20+
-------
21+
22+
Here is how you would implement depth-limiting on your schema.
23+
24+
.. code:: python
25+
from graphql import validate, parse
26+
from graphene import ObjectType, Schema, String
27+
from graphene.validation import depth_limit_validator
28+
29+
30+
class MyQuery(ObjectType):
31+
name = String(required=True)
32+
33+
34+
schema = Schema(query=MyQuery)
35+
36+
# queries which have a depth more than 20
37+
# will not be executed.
38+
39+
validation_errors = validate(
40+
schema=schema,
41+
document_ast=parse('THE QUERY'),
42+
rules=(
43+
depth_limit_validator(
44+
max_depth=20
45+
),
46+
)
47+
)
48+
49+
50+
Disable Introspection
51+
---------------------
52+
the disable introspection validation rule ensures that your schema cannot be introspected.
53+
This is a useful security measure in production environments.
54+
55+
Usage
56+
-------
57+
58+
Here is how you would disable introspection for your schema.
59+
60+
.. code:: python
61+
from graphql import validate, parse
62+
from graphene import ObjectType, Schema, String
63+
from graphene.validation import DisableIntrospection
64+
65+
66+
class MyQuery(ObjectType):
67+
name = String(required=True)
68+
69+
70+
schema = Schema(query=MyQuery)
71+
72+
# introspection queries will not be executed.
73+
74+
validation_errors = validate(
75+
schema=schema,
76+
document_ast=parse('THE QUERY'),
77+
rules=(
78+
DisableIntrospection,
79+
)
80+
)
81+
82+
83+
Implementing custom validators
84+
------------------------------
85+
All custom query validators should extend the `ValidationRule <https://github.com/graphql-python/graphql-core/blob/v3.0.5/src/graphql/validation/rules/__init__.py#L37>`_
86+
base class importable from the graphql.validation.rules module. Query validators are visitor classes. They are
87+
instantiated at the time of query validation with one required argument (context: ASTValidationContext). In order to
88+
perform validation, your validator class should define one or more of enter_* and leave_* methods. For possible
89+
enter/leave items as well as details on function documentation, please see contents of the visitor module. To make
90+
validation fail, you should call validator's report_error method with the instance of GraphQLError describing failure
91+
reason. Here is an example query validator that visits field definitions in GraphQL query and fails query validation
92+
if any of those fields are blacklisted:
93+
94+
.. code:: python
95+
from graphql import GraphQLError
96+
from graphql.language import FieldNode
97+
from graphql.validation import ValidationRule
98+
99+
100+
my_blacklist = (
101+
"disallowed_field",
102+
)
103+
104+
105+
def is_blacklisted_field(field_name: str):
106+
return field_name.lower() in my_blacklist
107+
108+
109+
class BlackListRule(ValidationRule):
110+
def enter_field(self, node: FieldNode, *_args):
111+
field_name = node.name.value
112+
if not is_blacklisted_field(field_name):
113+
return
114+
115+
self.report_error(
116+
GraphQLError(
117+
f"Cannot query '{field_name}': field is blacklisted.", node,
118+
)
119+
)
120+

graphene/types/schema.py

Lines changed: 0 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -393,108 +393,11 @@ def resolve_type(self, resolve_type_func, type_name, root, info, _type):
393393
return type_
394394

395395

396-
class UnforgivingExecutionContext(ExecutionContext):
397-
"""An execution context which doesn't swallow exceptions.
398-
399-
The only difference between this execution context and the one it inherits from is
400-
that ``except Exception`` is commented out within ``resolve_field_value_or_error``.
401-
By removing that exception handling, only ``GraphQLError``'s are caught.
402-
"""
403-
404-
def resolve_field_value_or_error(
405-
self, field_def, field_nodes, resolve_fn, source, info
406-
):
407-
"""Resolve field to a value or an error.
408-
409-
Isolates the "ReturnOrAbrupt" behavior to not de-opt the resolve_field()
410-
method. Returns the result of resolveFn or the abrupt-return Error object.
411-
412-
For internal use only.
413-
"""
414-
try:
415-
# Build a dictionary of arguments from the field.arguments AST, using the
416-
# variables scope to fulfill any variable references.
417-
args = get_argument_values(field_def, field_nodes[0], self.variable_values)
418-
419-
# Note that contrary to the JavaScript implementation, we pass the context
420-
# value as part of the resolve info.
421-
result = resolve_fn(source, info, **args)
422-
if self.is_awaitable(result):
423-
# noinspection PyShadowingNames
424-
async def await_result():
425-
try:
426-
return await result
427-
except GraphQLError as error:
428-
return error
429-
# except Exception as error:
430-
# return GraphQLError(str(error), original_error=error)
431-
432-
# Yes, this is commented out code. It's been intentionally
433-
# _not_ removed to show what has changed from the original
434-
# implementation.
435-
436-
return await_result()
437-
return result
438-
except GraphQLError as error:
439-
return error
440-
# except Exception as error:
441-
# return GraphQLError(str(error), original_error=error)
442-
443-
# Yes, this is commented out code. It's been intentionally _not_
444-
# removed to show what has changed from the original implementation.
445-
446-
def complete_value_catching_error(
447-
self, return_type, field_nodes, info, path, result
448-
):
449-
"""Complete a value while catching an error.
450-
451-
This is a small wrapper around completeValue which detects and logs errors in
452-
the execution context.
453-
"""
454-
try:
455-
if self.is_awaitable(result):
456-
457-
async def await_result():
458-
value = self.complete_value(
459-
return_type, field_nodes, info, path, await result
460-
)
461-
if self.is_awaitable(value):
462-
return await value
463-
return value
464-
465-
completed = await_result()
466-
else:
467-
completed = self.complete_value(
468-
return_type, field_nodes, info, path, result
469-
)
470-
if self.is_awaitable(completed):
471-
# noinspection PyShadowingNames
472-
async def await_completed():
473-
try:
474-
return await completed
475-
476-
# CHANGE WAS MADE HERE
477-
# ``GraphQLError`` was swapped in for ``except Exception``
478-
except GraphQLError as error:
479-
self.handle_field_error(error, field_nodes, path, return_type)
480-
481-
return await_completed()
482-
return completed
483-
484-
# CHANGE WAS MADE HERE
485-
# ``GraphQLError`` was swapped in for ``except Exception``
486-
except GraphQLError as error:
487-
self.handle_field_error(error, field_nodes, path, return_type)
488-
return None
489-
490-
491396
class Schema:
492397
"""Schema Definition.
493-
494398
A Graphene Schema can execute operations (query, mutation, subscription) against the defined
495399
types. For advanced purposes, the schema can be used to lookup type definitions and answer
496400
questions about the types through introspection.
497-
498401
Args:
499402
query (Type[ObjectType]): Root query *ObjectType*. Describes entry point for fields to *read*
500403
data in your Schema.
@@ -541,7 +444,6 @@ def __getattr__(self, type_name):
541444
"""
542445
This function let the developer select a type in a given schema
543446
by accessing its attrs.
544-
545447
Example: using schema.Query for accessing the "Query" type in the Schema
546448
"""
547449
_type = self.graphql_schema.get_type(type_name)
@@ -556,11 +458,9 @@ def lazy(self, _type):
556458

557459
def execute(self, *args, **kwargs):
558460
"""Execute a GraphQL query on the schema.
559-
560461
Use the `graphql_sync` function from `graphql-core` to provide the result
561462
for a query string. Most of the time this method will be called by one of the Graphene
562463
:ref:`Integrations` via a web request.
563-
564464
Args:
565465
request_string (str or Document): GraphQL request (query, mutation or subscription)
566466
as string or parsed AST form from `graphql-core`.
@@ -577,7 +477,6 @@ def execute(self, *args, **kwargs):
577477
defined in `graphql-core`.
578478
execution_context_class (ExecutionContext, optional): The execution context class
579479
to use when resolving queries and mutations.
580-
581480
Returns:
582481
:obj:`ExecutionResult` containing any data and errors for the operation.
583482
"""
@@ -586,7 +485,6 @@ def execute(self, *args, **kwargs):
586485

587486
async def execute_async(self, *args, **kwargs):
588487
"""Execute a GraphQL query on the schema asynchronously.
589-
590488
Same as `execute`, but uses `graphql` instead of `graphql_sync`.
591489
"""
592490
kwargs = normalize_execute_kwargs(kwargs)

0 commit comments

Comments
 (0)