From 46f21840610be6ddb3569a2b96b7866ae9f33e12 Mon Sep 17 00:00:00 2001 From: David Vogt Date: Thu, 8 Feb 2024 17:32:36 +0100 Subject: [PATCH] fix(validation): do not block __typename when introspection is disabled The `DisableIntrospection` validator rejects everything that could lead to insight into the schema. Sadly, our frontends rely on having `__typename` available, thus we need our own validator that allows this specific introspection key (but not anything else) --- caluma/caluma_user/tests/test_views.py | 57 ++++++++++++++++++++++++++ caluma/caluma_user/views.py | 17 +++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/caluma/caluma_user/tests/test_views.py b/caluma/caluma_user/tests/test_views.py index 27cb751b0..2f6a094e6 100644 --- a/caluma/caluma_user/tests/test_views.py +++ b/caluma/caluma_user/tests/test_views.py @@ -62,3 +62,60 @@ def test_authentication_view_improperly_configured(rf, settings): request = rf.get("/graphql", HTTP_AUTHORIZATION="Bearer Token") with pytest.raises(ImproperlyConfigured): views.AuthenticationGraphQLView.as_view()(request) + + +@pytest.mark.parametrize( + "introspect, suppress_introspection, expect_error", + [ + ("schema", False, False), + ("schema", True, True), + ("typename", False, False), + ("typename", True, False), + ("nothing", False, False), + ("nothing", True, False), + ], +) +def test_suppressed_introspection( + rf, + requests_mock, + settings, + mocker, + # params + introspect, + expect_error, + suppress_introspection, +): + userinfo = {"sub": "1"} + requests_mock.get(settings.OIDC_USERINFO_ENDPOINT, text=json.dumps(userinfo)) + + if suppress_introspection: + mocker.patch( + "caluma.caluma_user.views.AuthenticationGraphQLView.validation_rules", + (views.SuppressIntrospection,), + ) + + schema_subquery = "__schema { description }" if introspect == "schema" else "" + inspection_key = "__typename" if introspect == "typename" else "" + + # Query gets spiked with some introspection keys depending on parametrisation + query = f""" + query ds {{ + {schema_subquery} + allDataSources {{ + {inspection_key} + totalCount + edges {{ + node {{ + {inspection_key} + info + }} + }} + }} + }} + """ + + request = rf.post("/graphql", {"query": query, "variables": {}}) + response = views.AuthenticationGraphQLView.as_view()(request) + resp_json = json.loads(response.content) + + assert ("errors" in resp_json) == expect_error diff --git a/caluma/caluma_user/views.py b/caluma/caluma_user/views.py index 03a5fd3b3..9e9d2da3a 100644 --- a/caluma/caluma_user/views.py +++ b/caluma/caluma_user/views.py @@ -19,9 +19,24 @@ class HttpResponseUnauthorized(HttpResponse): status_code = 401 +class SuppressIntrospection(DisableIntrospection): + """Validate request to reject any introspection, except the bare minimum.""" + + # The base class, graphene.validation.DisableIntrospection, is too strict: + # It rejects everything starting with double underscores (as per GQL spec) + # but we need `__typename` for our frontends to work correctly. + + ALLOWED_INTROSPECTION_KEYS = ["__typename"] + + def enter_field(self, node, *_args): + field_name = node.name.value + if field_name not in self.ALLOWED_INTROSPECTION_KEYS: + super().enter_field(node, *_args) + + custom_validation_rules = [] if settings.DISABLE_INTROSPECTION: # pragma: no cover - custom_validation_rules.append(DisableIntrospection) + custom_validation_rules.append(SuppressIntrospection) if settings.QUERY_DEPTH_LIMIT: # pragma: no cover custom_validation_rules.append(