From 1e14f6a5e6148d625d0951e17052a0f688b6a16b Mon Sep 17 00:00:00 2001 From: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> Date: Thu, 23 Jul 2026 00:13:05 -0600 Subject: [PATCH] fix(api): don't scope serializer querysets by AnonymousUser during schema generation The Finding and import serializers scope their endpoint/location PrimaryKeyRelatedField querysets to request.user. During OpenAPI schema generation drf-spectacular instantiates serializers with an AnonymousUser request, which is truthy, so the `if user` branch called get_authorized_*(user=AnonymousUser); filtering a PK field by the AnonymousUser instance raised "Field 'id' expected a number but got AnonymousUser". drf-spectacular then dropped the affected views, producing an operationId collision and a KeyError in the prefetch postprocessing hook. Treat an unauthenticated user as no user so the field falls back to an empty queryset, matching pre-existing behavior for anonymous/schema-time contexts. Add a regression test that renders the full OpenAPI v3 schema and gates that generation produces zero errors (drf-spectacular logs, rather than raises, when it drops a view -- so nothing caught this before). Co-Authored-By: Claude Opus 4.8 (1M context) --- dojo/api_v2/serializers.py | 6 ++++++ dojo/finding/api/serializer.py | 6 ++++++ unittests/test_rest_framework.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index 5565f9986ac..3f4818f4cd8 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -518,6 +518,12 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Scope endpoint_to_add to the locations/endpoints the requesting user is authorized for. user = getattr(self.context.get("request"), "user", None) + # An unauthenticated user (e.g. AnonymousUser during OpenAPI schema + # generation, where the request has no real user) is truthy but cannot + # be authorization-scoped; treat it as no user so the field falls back to + # an empty queryset instead of raising on the AnonymousUser instance. + if user is not None and not user.is_authenticated: + user = None if not settings.V3_FEATURE_LOCATIONS: # TODO: why do we allow only existing endpoints? self.fields["endpoint_to_add"] = serializers.PrimaryKeyRelatedField( diff --git a/dojo/finding/api/serializer.py b/dojo/finding/api/serializer.py index b03f1e60930..f70494dc716 100644 --- a/dojo/finding/api/serializer.py +++ b/dojo/finding/api/serializer.py @@ -354,6 +354,12 @@ def __init__(self, *args, **kwargs): # Scope the endpoints field to the references the requesting user is authorized # for, mirroring the scoping the finding UI form already applies. user = getattr(self.context.get("request"), "user", None) + # An unauthenticated user (e.g. AnonymousUser during OpenAPI schema + # generation, where the request has no real user) is truthy but cannot + # be authorization-scoped; treat it as no user so the field falls back to + # an empty queryset instead of raising on the AnonymousUser instance. + if user is not None and not user.is_authenticated: + user = None if not settings.V3_FEATURE_LOCATIONS: self.fields["endpoints"] = serializers.PrimaryKeyRelatedField( many=True, required=False, diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py index c0ce346bfa4..892b6533dd4 100644 --- a/unittests/test_rest_framework.py +++ b/unittests/test_rest_framework.py @@ -4168,3 +4168,35 @@ def __init__(self, *args, **kwargs): self.test_type = TestType.STANDARD self.deleted_objects = 1 BaseClass.RESTEndpointTest.__init__(self, *args, **kwargs) + + +class OpenAPISchemaGenerationTest(DojoAPITestCase): + + """ + Render the full OpenAPI v3 schema and gate that generation is error-free. + + drf-spectacular does not raise when it fails to introspect a view or + serializer -- it logs an error and drops the operation from the schema, so a + broken endpoint (e.g. a serializer whose field querysets raise for the + unauthenticated request used at schema time) silently ships incomplete API + docs and can trip downstream postprocessing. Fail the build if schema + generation produces any error. + """ + + def test_schema_generation_produces_no_errors(self): + GENERATOR_STATS.reset() + generator = spectacular_settings.DEFAULT_GENERATOR_CLASS() + schema = generator.get_schema(request=None, public=True) + + # Structurally valid per the OpenAPI 3 spec ... + validate_schema(schema) + + # ... and generated without drf-spectacular logging any error (a logged + # error means an operation was silently dropped from the schema). + errors = list(GENERATOR_STATS._error_cache) + self.assertEqual( + errors, + [], + f"OpenAPI schema generation produced {len(errors)} error(s):\n" + + "\n".join(str(error) for error in errors), + )