diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index 5565f9986a..3f4818f4cd 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 b03f1e6093..f70494dc71 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 c0ce346bfa..892b6533dd 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), + )