Skip to content

Commit

Permalink
Merge pull request #1357 from codebyaryan/master
Browse files Browse the repository at this point in the history
add support for query validation
  • Loading branch information
syrusakbary committed Aug 21, 2021
2 parents fce45ef + 74a6565 commit efc0353
Show file tree
Hide file tree
Showing 15 changed files with 759 additions and 257 deletions.
File renamed without changes.
83 changes: 83 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: 📄 Tests
on:
push:
branches:
- master
- '*.x'
paths-ignore:
- 'docs/**'
- '*.md'
- '*.rst'
pull_request:
branches:
- master
- '*.x'
paths-ignore:
- 'docs/**'
- '*.md'
- '*.rst'
jobs:
tests:
# runs the test suite
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
- {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python }}

- name: update pip
run: |
pip install -U wheel
pip install -U setuptools
python -m pip install -U pip
- name: get pip cache dir
id: pip-cache
run: echo "::set-output name=dir::$(pip cache dir)"

- name: cache pip dependencies
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }}

- run: pip install tox
- run: tox -e ${{ matrix.tox }}

coveralls_finish:
# check coverage increase/decrease
needs: tests
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: AndreMiras/coveralls-python-action@develop

deploy:
# builds and publishes to PyPi
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.7'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
File renamed without changes.
42 changes: 0 additions & 42 deletions .travis.yml

This file was deleted.

1 change: 1 addition & 0 deletions docs/execution/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Execution
dataloader
fileuploading
subscriptions
queryvalidation
120 changes: 120 additions & 0 deletions docs/execution/queryvalidation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
Query Validation
==========
GraphQL uses query validators to check if Query AST is valid and can be executed. Every GraphQL server implements
standard query validators. For example, there is an validator that tests if queried field exists on queried type, that
makes query fail with "Cannot query field on type" error if it doesn't.

To help with common use cases, graphene provides a few validation rules out of the box.


Depth limit Validator
-----------------
The depth limit validator helps to prevent execution of malicious
queries. It takes in the following arguments.

- ``max_depth`` is the maximum allowed depth for any operation in a GraphQL document.
- ``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
- ``callback`` Called each time validation runs. Receives an Object which is a map of the depths for each operation.

Usage
-------

Here is how you would implement depth-limiting on your schema.

.. code:: python
from graphql import validate, parse
from graphene import ObjectType, Schema, String
from graphene.validation import depth_limit_validator
class MyQuery(ObjectType):
name = String(required=True)
schema = Schema(query=MyQuery)
# queries which have a depth more than 20
# will not be executed.
validation_errors = validate(
schema=schema,
document_ast=parse('THE QUERY'),
rules=(
depth_limit_validator(
max_depth=20
),
)
)
Disable Introspection
---------------------
the disable introspection validation rule ensures that your schema cannot be introspected.
This is a useful security measure in production environments.

Usage
-------

Here is how you would disable introspection for your schema.

.. code:: python
from graphql import validate, parse
from graphene import ObjectType, Schema, String
from graphene.validation import DisableIntrospection
class MyQuery(ObjectType):
name = String(required=True)
schema = Schema(query=MyQuery)
# introspection queries will not be executed.
validation_errors = validate(
schema=schema,
document_ast=parse('THE QUERY'),
rules=(
DisableIntrospection,
)
)
Implementing custom validators
------------------------------
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>`_
base class importable from the graphql.validation.rules module. Query validators are visitor classes. They are
instantiated at the time of query validation with one required argument (context: ASTValidationContext). In order to
perform validation, your validator class should define one or more of enter_* and leave_* methods. For possible
enter/leave items as well as details on function documentation, please see contents of the visitor module. To make
validation fail, you should call validator's report_error method with the instance of GraphQLError describing failure
reason. Here is an example query validator that visits field definitions in GraphQL query and fails query validation
if any of those fields are blacklisted:

.. code:: python
from graphql import GraphQLError
from graphql.language import FieldNode
from graphql.validation import ValidationRule
my_blacklist = (
"disallowed_field",
)
def is_blacklisted_field(field_name: str):
return field_name.lower() in my_blacklist
class BlackListRule(ValidationRule):
def enter_field(self, node: FieldNode, *_args):
field_name = node.name.value
if not is_blacklisted_field(field_name):
return
self.report_error(
GraphQLError(
f"Cannot query '{field_name}': field is blacklisted.", node,
)
)
102 changes: 0 additions & 102 deletions graphene/types/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,108 +393,11 @@ def resolve_type(self, resolve_type_func, type_name, root, info, _type):
return type_


class UnforgivingExecutionContext(ExecutionContext):
"""An execution context which doesn't swallow exceptions.
The only difference between this execution context and the one it inherits from is
that ``except Exception`` is commented out within ``resolve_field_value_or_error``.
By removing that exception handling, only ``GraphQLError``'s are caught.
"""

def resolve_field_value_or_error(
self, field_def, field_nodes, resolve_fn, source, info
):
"""Resolve field to a value or an error.
Isolates the "ReturnOrAbrupt" behavior to not de-opt the resolve_field()
method. Returns the result of resolveFn or the abrupt-return Error object.
For internal use only.
"""
try:
# Build a dictionary of arguments from the field.arguments AST, using the
# variables scope to fulfill any variable references.
args = get_argument_values(field_def, field_nodes[0], self.variable_values)

# Note that contrary to the JavaScript implementation, we pass the context
# value as part of the resolve info.
result = resolve_fn(source, info, **args)
if self.is_awaitable(result):
# noinspection PyShadowingNames
async def await_result():
try:
return await result
except GraphQLError as error:
return error
# except Exception as error:
# return GraphQLError(str(error), original_error=error)

# Yes, this is commented out code. It's been intentionally
# _not_ removed to show what has changed from the original
# implementation.

return await_result()
return result
except GraphQLError as error:
return error
# except Exception as error:
# return GraphQLError(str(error), original_error=error)

# Yes, this is commented out code. It's been intentionally _not_
# removed to show what has changed from the original implementation.

def complete_value_catching_error(
self, return_type, field_nodes, info, path, result
):
"""Complete a value while catching an error.
This is a small wrapper around completeValue which detects and logs errors in
the execution context.
"""
try:
if self.is_awaitable(result):

async def await_result():
value = self.complete_value(
return_type, field_nodes, info, path, await result
)
if self.is_awaitable(value):
return await value
return value

completed = await_result()
else:
completed = self.complete_value(
return_type, field_nodes, info, path, result
)
if self.is_awaitable(completed):
# noinspection PyShadowingNames
async def await_completed():
try:
return await completed

# CHANGE WAS MADE HERE
# ``GraphQLError`` was swapped in for ``except Exception``
except GraphQLError as error:
self.handle_field_error(error, field_nodes, path, return_type)

return await_completed()
return completed

# CHANGE WAS MADE HERE
# ``GraphQLError`` was swapped in for ``except Exception``
except GraphQLError as error:
self.handle_field_error(error, field_nodes, path, return_type)
return None


class Schema:
"""Schema Definition.
A Graphene Schema can execute operations (query, mutation, subscription) against the defined
types. For advanced purposes, the schema can be used to lookup type definitions and answer
questions about the types through introspection.
Args:
query (Type[ObjectType]): Root query *ObjectType*. Describes entry point for fields to *read*
data in your Schema.
Expand Down Expand Up @@ -541,7 +444,6 @@ def __getattr__(self, type_name):
"""
This function let the developer select a type in a given schema
by accessing its attrs.
Example: using schema.Query for accessing the "Query" type in the Schema
"""
_type = self.graphql_schema.get_type(type_name)
Expand All @@ -556,11 +458,9 @@ def lazy(self, _type):

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

async def execute_async(self, *args, **kwargs):
"""Execute a GraphQL query on the schema asynchronously.
Same as `execute`, but uses `graphql` instead of `graphql_sync`.
"""
kwargs = normalize_execute_kwargs(kwargs)
Expand Down

0 comments on commit efc0353

Please sign in to comment.