Repository: graphql-python/graphene-django

Issue:
How to disable introspection via validation rules?
**Is your feature request related to a problem? Please describe.**
I need to disable introspection for graphql, how would I do that using the method described [here](https://docs.graphene-python.org/en/latest/execution/queryvalidation/#disable-introspection)

**Describe the solution you'd like**
An example of accomplishing this would be nice

**Describe alternatives you've considered**
I am thinking to either create a subclass of `GraphQLView` or `graphene.Schema` and add the query validation there, but I don't know if that's the recommended approach

**Additional context**


Discussion:
I ended up going with this:
```py
from graphql import ExecutionResult, parse, validate
from graphql.validation import NoSchemaIntrospectionCustomRule

class ValidatingSchema(graphene.Schema):
    def __init__(self, *args, validation_rules=(), **kwargs):
        super().__init__(*args, **kwargs)
        self.validation_rules = validation_rules

    def execute(self, *args, **kwargs):
        return self.validate(*args, **kwargs) or super().execute(*args, **kwargs)

    async def execute_async(self, *args, **kwargs):
        return self.validate(*args, **kwargs) or await super().execute_async(*args, **kwargs)

    def validate(self, *args, **kwargs):
        if query := (kwargs.get("source") or kwargs.get("request_string")):
            errors = validate(self.graphql_schema, parse(query), rules=self.validation_rules, max_errors=3)
            if errors:
                return ExecutionResult(errors=errors)


schema = ValidatingSchema(
    query=...,
    mutation=...,
    directives=...,
    validation_rules=(
        *filter(None, (NoSchemaIntrospectionCustomRule if not settings.DEBUG else None,)),
    ),
)
```

Resolving patch:
diff --git a/graphene_django/settings.py b/graphene_django/settings.py
--- a/graphene_django/settings.py
+++ b/graphene_django/settings.py
@@ -43,6 +43,7 @@
     "GRAPHIQL_INPUT_VALUE_DEPRECATION": False,
     "ATOMIC_MUTATIONS": False,
     "TESTING_ENDPOINT": "/graphql",
+    "MAX_VALIDATION_ERRORS": None,
 }
 
 if settings.DEBUG:
diff --git a/graphene_django/views.py b/graphene_django/views.py
--- a/graphene_django/views.py
+++ b/graphene_django/views.py
@@ -96,6 +96,7 @@ class GraphQLView(View):
     batch = False
     subscription_path = None
     execution_context_class = None
+    validation_rules = None
 
     def __init__(
         self,
@@ -107,6 +108,7 @@ def __init__(
         batch=False,
         subscription_path=None,
         execution_context_class=None,
+        validation_rules=None,
     ):
         if not schema:
             schema = graphene_settings.SCHEMA
@@ -135,6 +137,8 @@ def __init__(
         ), "A Schema is required to be provided to GraphQLView."
         assert not all((graphiql, batch)), "Use either graphiql or batch processing"
 
+        self.validation_rules = validation_rules or self.validation_rules
+
     # noinspection PyUnusedLocal
     def get_root_value(self, request):
         return self.root_value
@@ -332,7 +336,12 @@ def execute_graphql_request(
                 )
             )
 
-        validation_errors = validate(schema, document)
+        validation_errors = validate(
+            schema,
+            document,
+            self.validation_rules,
+            graphene_settings.MAX_VALIDATION_ERRORS,
+        )
 
         if validation_errors:
             return ExecutionResult(data=None, errors=validation_errors)


Test patch:
diff --git a/graphene_django/tests/test_views.py b/graphene_django/tests/test_views.py
--- a/graphene_django/tests/test_views.py
+++ b/graphene_django/tests/test_views.py
@@ -827,3 +827,97 @@ def test_query_errors_atomic_request(set_rollback_mock, client):
 def test_query_errors_non_atomic(set_rollback_mock, client):
     client.get(url_string(query="force error"))
     set_rollback_mock.assert_not_called()
+
+
+VALIDATION_URLS = [
+    "/graphql/validation/",
+    "/graphql/validation/alternative/",
+    "/graphql/validation/inherited/",
+]
+
+QUERY_WITH_TWO_INTROSPECTIONS = """
+query Instrospection {
+    queryType: __schema {
+        queryType {name}
+    }
+    mutationType: __schema {
+        mutationType {name}
+    }
+}
+"""
+
+N_INTROSPECTIONS = 2
+
+INTROSPECTION_DISALLOWED_ERROR_MESSAGE = "introspection is disabled"
+MAX_VALIDATION_ERRORS_EXCEEDED_MESSAGE = "too many validation errors"
+
+
+@pytest.mark.urls("graphene_django.tests.urls_validation")
+def test_allow_introspection(client):
+    response = client.post(
+        url_string("/graphql/", query="{__schema {queryType {name}}}")
+    )
+    assert response.status_code == 200
+
+    assert response_json(response) == {
+        "data": {"__schema": {"queryType": {"name": "QueryRoot"}}}
+    }
+
+
+@pytest.mark.parametrize("url", VALIDATION_URLS)
+@pytest.mark.urls("graphene_django.tests.urls_validation")
+def test_validation_disallow_introspection(client, url):
+    response = client.post(url_string(url, query="{__schema {queryType {name}}}"))
+
+    assert response.status_code == 400
+
+    json_response = response_json(response)
+    assert "data" not in json_response
+    assert "errors" in json_response
+    assert len(json_response["errors"]) == 1
+
+    error_message = json_response["errors"][0]["message"]
+    assert INTROSPECTION_DISALLOWED_ERROR_MESSAGE in error_message
+
+
+@pytest.mark.parametrize("url", VALIDATION_URLS)
+@pytest.mark.urls("graphene_django.tests.urls_validation")
+@patch(
+    "graphene_django.settings.graphene_settings.MAX_VALIDATION_ERRORS", N_INTROSPECTIONS
+)
+def test_within_max_validation_errors(client, url):
+    response = client.post(url_string(url, query=QUERY_WITH_TWO_INTROSPECTIONS))
+
+    assert response.status_code == 400
+
+    json_response = response_json(response)
+    assert "data" not in json_response
+    assert "errors" in json_response
+    assert len(json_response["errors"]) == N_INTROSPECTIONS
+
+    error_messages = [error["message"].lower() for error in json_response["errors"]]
+
+    n_introspection_error_messages = sum(
+        INTROSPECTION_DISALLOWED_ERROR_MESSAGE in msg for msg in error_messages
+    )
+    assert n_introspection_error_messages == N_INTROSPECTIONS
+
+    assert all(
+        MAX_VALIDATION_ERRORS_EXCEEDED_MESSAGE not in msg for msg in error_messages
+    )
+
+
+@pytest.mark.parametrize("url", VALIDATION_URLS)
+@pytest.mark.urls("graphene_django.tests.urls_validation")
+@patch("graphene_django.settings.graphene_settings.MAX_VALIDATION_ERRORS", 1)
+def test_exceeds_max_validation_errors(client, url):
+    response = client.post(url_string(url, query=QUERY_WITH_TWO_INTROSPECTIONS))
+
+    assert response.status_code == 400
+
+    json_response = response_json(response)
+    assert "data" not in json_response
+    assert "errors" in json_response
+
+    error_messages = (error["message"].lower() for error in json_response["errors"])
+    assert any(MAX_VALIDATION_ERRORS_EXCEEDED_MESSAGE in msg for msg in error_messages)
diff --git a/graphene_django/tests/urls_validation.py b/graphene_django/tests/urls_validation.py
new file mode 100644
--- /dev/null
+++ b/graphene_django/tests/urls_validation.py
@@ -0,0 +1,26 @@
+from django.urls import path
+
+from graphene.validation import DisableIntrospection
+
+from ..views import GraphQLView
+from .schema_view import schema
+
+
+class View(GraphQLView):
+    schema = schema
+
+
+class NoIntrospectionView(View):
+    validation_rules = (DisableIntrospection,)
+
+
+class NoIntrospectionViewInherited(NoIntrospectionView):
+    pass
+
+
+urlpatterns = [
+    path("graphql/", View.as_view()),
+    path("graphql/validation/", View.as_view(validation_rules=(DisableIntrospection,))),
+    path("graphql/validation/alternative/", NoIntrospectionView.as_view()),
+    path("graphql/validation/inherited/", NoIntrospectionViewInherited.as_view()),
+]