Просмотр исходного кода

Fixes #23423: Represent 64-bit integer fields as BigInt in GraphQL

strawberry-django maps BigIntegerField and PositiveBigIntegerField to Int,
which GraphQL limits to 32 bits, so a query returning an object ID above
2,147,483,647 failed outright. Map both field types to the BigInt scalar,
and switch the data_source_id, data_file_id and L2VPN termination
assigned_object_id filters to BigInt lookups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jeremy Stretch 5 дней назад
Родитель
Сommit
56592e9dc2

+ 4 - 2
netbox/netbox/graphql/filter_mixins.py

@@ -5,6 +5,8 @@ import strawberry
 import strawberry_django
 from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup, StrFilterLookup
 
+from netbox.graphql.scalars import BigInt
+
 __all__ = (
     'DistanceFilterMixin',
     'ImageAttachmentFilterMixin',
@@ -42,11 +44,11 @@ class SyncedDataFilterMixin:
     data_source: Annotated['DataSourceFilter', strawberry.lazy('core.graphql.filters')] | None = (
         strawberry_django.filter_field()
     )
-    data_source_id: FilterLookup[int] | None = strawberry_django.filter_field()
+    data_source_id: FilterLookup[BigInt] | None = strawberry_django.filter_field()
     data_file: Annotated['DataFileFilter', strawberry.lazy('core.graphql.filters')] | None = (
         strawberry_django.filter_field()
     )
-    data_file_id: FilterLookup[int] | None = strawberry_django.filter_field()
+    data_file_id: FilterLookup[BigInt] | None = strawberry_django.filter_field()
     data_path: StrFilterLookup | None = strawberry_django.filter_field()
     auto_sync_enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
     data_synced: DatetimeFilterLookup | None = strawberry_django.filter_field()

+ 9 - 0
netbox/netbox/settings.py

@@ -1039,11 +1039,14 @@ for plugin_name in PLUGINS:
 # Monkey-patching
 #
 
+from django.db import models  # noqa: E402
 from rest_framework.utils import field_mapping  # noqa: E402
 from strawberry_django import pagination  # noqa: E402
 from strawberry_django.fields.field import StrawberryDjangoField  # noqa: E402
+from strawberry_django.fields.types import field_type_map  # noqa: E402
 
 from netbox.graphql.pagination import OffsetPaginationInput, apply_pagination  # noqa: E402
+from netbox.graphql.scalars import BigInt  # noqa: E402
 
 # TODO: Remove this once #20547 has been implemented
 # Override DRF's get_unique_validators() function with our own (see bug #19302)
@@ -1055,6 +1058,12 @@ pagination.OffsetPaginationInput = OffsetPaginationInput
 # Patch StrawberryDjangoField to use our custom `apply_pagination()` method with support for cursor-based pagination
 StrawberryDjangoField.apply_pagination = apply_pagination
 
+# Map 64-bit integer model fields to the BigInt scalar, since GraphQL's Int type is limited to 32 bits (see #23423)
+field_type_map.update({
+    models.BigIntegerField: BigInt,
+    models.PositiveBigIntegerField: BigInt,
+})
+
 
 # UNSUPPORTED FUNCTIONALITY: Import any local overrides.
 try:

+ 65 - 3
netbox/netbox/tests/test_graphql.py

@@ -35,8 +35,8 @@ from dcim.models import (
     VirtualChassis,
 )
 from extras.choices import CustomFieldTypeChoices
-from extras.models import CustomField, TableConfig, Tag
-from ipam.models import RIR, Aggregate, IPAddress, Prefix
+from extras.models import CustomField, ExportTemplate, JournalEntry, TableConfig, Tag
+from ipam.models import RIR, VLAN, Aggregate, IPAddress, Prefix
 from netbox.graphql.pagination import apply_distinct_window_pagination
 from netbox.graphql.scalars import BigInt, BigIntScalar
 from netbox.graphql.schema import Query, get_schema_extensions, schema
@@ -47,6 +47,8 @@ from netbox.tests.dummy_plugin.models import DummySiteAttachment
 from users.models import ObjectPermission, User
 from utilities.tables import get_table_for_model
 from utilities.testing import APITestCase, APIViewTestCases, TestCase, disable_warnings
+from vpn.choices import L2VPNTypeChoices
+from vpn.models import L2VPN, L2VPNTermination
 
 
 def count_primary_table_queries(queries, table):
@@ -455,6 +457,62 @@ class GraphQLAPITestCase(APITestCase):
         self.assertNotIn('errors', data)
         self.assertEqual(len(data['data']['device_list']), 3)
 
+    def test_graphql_64bit_object_ids(self):
+        """
+        Object IDs beyond the 32-bit range must be accepted by GraphQL output fields and filters. Regression test
+        for #23423.
+        """
+        self.add_permissions(
+            'extras.view_journalentry',
+            'extras.view_exporttemplate',
+            'vpn.view_l2vpntermination',
+        )
+        url = reverse('graphql')
+        object_id = 3_000_000_000
+
+        site = Site.objects.create(id=object_id, name='Site 64-bit', slug='site-64-bit')
+        JournalEntry.objects.create(assigned_object=site, comments='Test')
+        vlan = VLAN.objects.create(id=object_id, vid=100, name='VLAN 100')
+        l2vpn = L2VPN.objects.create(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN)
+        L2VPNTermination.objects.create(l2vpn=l2vpn, assigned_object=vlan)
+        data_source = DataSource.objects.create(id=object_id, name='Data Source 1', type='local', source_url='/tmp')
+        export_template = ExportTemplate.objects.create(
+            name='Export Template 1',
+            template_code='{{ queryset }}',
+            data_source=data_source,
+        )
+        export_template.object_types.set([ObjectType.objects.get_for_model(Site)])
+
+        # Output field (PositiveBigIntegerField)
+        query = '{ journal_entry_list { assigned_object_id } }'
+        response = self.client.post(url, data={'query': query}, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        data = json.loads(response.content)
+        self.assertNotIn('errors', data)
+        self.assertEqual(data['data']['journal_entry_list'], [{'assigned_object_id': object_id}])
+
+        # Filter on a generic object ID
+        query = f"""
+        {{
+            l2vpn_termination_list(filters: {{assigned_object_id: {{filter_lookup: {{exact: {object_id}}}}}}}) {{
+                id
+            }}
+        }}
+        """
+        response = self.client.post(url, data={'query': query}, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        data = json.loads(response.content)
+        self.assertNotIn('errors', data)
+        self.assertEqual(len(data['data']['l2vpn_termination_list']), 1)
+
+        # Filter on a related object ID
+        query = f'{{ export_template_list(filters: {{data_source_id: {{exact: {object_id}}}}}) {{ id }} }}'
+        response = self.client.post(url, data={'query': query}, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+        data = json.loads(response.content)
+        self.assertNotIn('errors', data)
+        self.assertEqual(data['data']['export_template_list'], [{'id': str(export_template.pk)}])
+
     def test_graphql_array_length_lookup(self):
         """
         The public GraphQL ``length`` array lookup must map to Django's ``len`` transform.
@@ -860,7 +918,11 @@ class GraphQLAPITestCase(APITestCase):
         class PluginQuery:
             platforms: list[PluginPlatformType] = strawberry_django.field()
 
-        plugin_schema = strawberry.Schema(query=PluginQuery, extensions=[DjangoOptimizerExtension])
+        plugin_schema = strawberry.Schema(
+            query=PluginQuery,
+            config=StrawberryConfig(scalar_map={BigInt: BigIntScalar}),
+            extensions=[DjangoOptimizerExtension],
+        )
         self.user.is_superuser = True
         result = plugin_schema.execute_sync(
             '{ platforms { name display } }',

+ 1 - 1
netbox/vpn/graphql/filters.py

@@ -216,6 +216,6 @@ class L2VPNTerminationFilter(NetBoxModelFilter):
     assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
         strawberry_django.filter_field()
     )
-    assigned_object_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
+    assigned_object_id: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
         strawberry_django.filter_field()
     )