Compare commits

...

4 Commits

Author SHA1 Message Date
Jeremy Stretch
e5f6fc55ea Fixes #21181: Handle AuthenticationFailed exception on /media endpoint 2026-01-19 10:17:50 -05:00
Martin Hauser
6b7b38ee0a fix(users): Refactor object permission query logic
Simplifies the `OBJECTPERMISSION_OBJECT_TYPES` definition by adjusting
query filters and introducing new conditions for specific app labels
and models.

Fixes #21051
2026-01-19 09:30:36 -05:00
matthew-242
c8f17e06a2 Add support to filter on cached relations _location, _region, _site and _site_group to ScopedFilterMixin (#21162) 2026-01-19 09:09:03 -05:00
Jeremy Stretch
edace6aff4 Fixes #21166: Fix support for filtering on unsigned 32-bit integer values in GraphQL API (#21186)
* Fixes #21166: Fix support for filtering on unsigned 32-bit integer values in GraphQL API

* tunnel_id should also use BigIntegerLookup
2026-01-19 08:54:39 -05:00
6 changed files with 60 additions and 14 deletions

View File

@@ -13,6 +13,7 @@ if TYPE_CHECKING:
from netbox.graphql.filter_lookups import IntegerLookup from netbox.graphql.filter_lookups import IntegerLookup
from extras.graphql.filters import ConfigTemplateFilter from extras.graphql.filters import ConfigTemplateFilter
from ipam.graphql.filters import VLANFilter, VLANTranslationPolicyFilter from ipam.graphql.filters import VLANFilter, VLANTranslationPolicyFilter
from dcim.graphql.filters import LocationFilter, RegionFilter, SiteFilter, SiteGroupFilter
from .filters import * from .filters import *
__all__ = ( __all__ = (
@@ -35,6 +36,20 @@ class ScopedFilterMixin:
) )
scope_id: ID | None = strawberry_django.filter_field() scope_id: ID | None = strawberry_django.filter_field()
# Cached relations
_location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='location')
)
_region: Annotated['RegionFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='region')
)
_site_group: Annotated['SiteGroupFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='site_group')
)
_site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='site')
)
@dataclass @dataclass
class ComponentModelFilterMixin: class ComponentModelFilterMixin:

View File

@@ -20,7 +20,7 @@ from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin
from virtualization.models import VMInterface from virtualization.models import VMInterface
if TYPE_CHECKING: if TYPE_CHECKING:
from netbox.graphql.filter_lookups import IntegerLookup, IntegerRangeArrayLookup from netbox.graphql.filter_lookups import BigIntegerLookup, IntegerLookup, IntegerRangeArrayLookup
from circuits.graphql.filters import ProviderFilter from circuits.graphql.filters import ProviderFilter
from core.graphql.filters import ContentTypeFilter from core.graphql.filters import ContentTypeFilter
from dcim.graphql.filters import SiteFilter from dcim.graphql.filters import SiteFilter
@@ -53,7 +53,7 @@ __all__ = (
class ASNFilter(TenancyFilterMixin, PrimaryModelFilter): class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
rir_id: ID | None = strawberry_django.filter_field() rir_id: ID | None = strawberry_django.filter_field()
asn: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( asn: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
sites: ( sites: (
@@ -70,10 +70,10 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: FilterLookup[str] | None = strawberry_django.filter_field()
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
rir_id: ID | None = strawberry_django.filter_field() rir_id: ID | None = strawberry_django.filter_field()
start: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( start: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
end: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( end: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )

View File

@@ -19,8 +19,11 @@ from strawberry_django import (
process_filters, process_filters,
) )
from netbox.graphql.scalars import BigInt
__all__ = ( __all__ = (
'ArrayLookup', 'ArrayLookup',
'BigIntegerLookup',
'FloatArrayLookup', 'FloatArrayLookup',
'FloatLookup', 'FloatLookup',
'IntegerArrayLookup', 'IntegerArrayLookup',
@@ -78,6 +81,29 @@ class IntegerLookup:
return process_filters(filters=filters, queryset=queryset, info=info, prefix=prefix) return process_filters(filters=filters, queryset=queryset, info=info, prefix=prefix)
@strawberry.input(one_of=True, description='Lookup for BigInteger fields. Only one of the lookup fields can be set.')
class BigIntegerLookup:
filter_lookup: FilterLookup[BigInt] | None = strawberry_django.filter_field()
range_lookup: RangeLookup[BigInt] | None = strawberry_django.filter_field()
comparison_lookup: ComparisonFilterLookup[BigInt] | None = strawberry_django.filter_field()
def get_filter(self):
for field in self.__strawberry_definition__.fields:
value = getattr(self, field.name, None)
if value is not strawberry.UNSET:
return value
return None
@strawberry_django.filter_field
def filter(self, info: Info, queryset: QuerySet, prefix: DirectiveValue[str] = '') -> Tuple[QuerySet, Q]:
filters = self.get_filter()
if not filters:
return queryset, Q()
return process_filters(filters=filters, queryset=queryset, info=info, prefix=prefix)
@strawberry.input(one_of=True, description='Lookup for Float fields. Only one of the lookup fields can be set.') @strawberry.input(one_of=True, description='Lookup for Float fields. Only one of the lookup fields can be set.')
class FloatLookup: class FloatLookup:
filter_lookup: FilterLookup[float] | None = strawberry_django.filter_field() filter_lookup: FilterLookup[float] | None = strawberry_django.filter_field()

View File

@@ -3,9 +3,10 @@ import string
from django.db.models import Q from django.db.models import Q
OBJECTPERMISSION_OBJECT_TYPES = Q( OBJECTPERMISSION_OBJECT_TYPES = (
~Q(app_label__in=['account', 'admin', 'auth', 'contenttypes', 'sessions', 'taggit', 'users']) | (Q(public=True) & ~Q(app_label='core', model='objecttype'))
Q(app_label='users', model__in=['objectpermission', 'token', 'group', 'user', 'owner']) | Q(app_label='core', model__in=['managedfile'])
| Q(app_label='extras', model__in=['scriptmodule', 'taggeditem'])
) )
CONSTRAINT_TOKEN_USER = '$user' CONSTRAINT_TOKEN_USER = '$user'

View File

@@ -5,9 +5,11 @@ from django.conf import settings
from django.contrib.auth.mixins import AccessMixin from django.contrib.auth.mixins import AccessMixin
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db.models import QuerySet from django.db.models import QuerySet
from django.http import HttpResponseForbidden
from django.urls import reverse from django.urls import reverse
from django.urls.exceptions import NoReverseMatch from django.urls.exceptions import NoReverseMatch
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import AuthenticationFailed
from netbox.api.authentication import TokenAuthentication from netbox.api.authentication import TokenAuthentication
from netbox.plugins import PluginConfig from netbox.plugins import PluginConfig
@@ -50,10 +52,12 @@ class TokenConditionalLoginRequiredMixin(ConditionalLoginRequiredMixin):
# Attempt to authenticate the user using a DRF token, if provided # Attempt to authenticate the user using a DRF token, if provided
if settings.LOGIN_REQUIRED and not request.user.is_authenticated: if settings.LOGIN_REQUIRED and not request.user.is_authenticated:
authenticator = TokenAuthentication() authenticator = TokenAuthentication()
auth_info = authenticator.authenticate(request) try:
if auth_info is not None: if auth_info := authenticator.authenticate(request) is not None:
request.user = auth_info[0] # User object request.user = auth_info[0] # User object
request.auth = auth_info[1] request.auth = auth_info[1]
except AuthenticationFailed:
return HttpResponseForbidden("Invalid token")
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)

View File

@@ -15,7 +15,7 @@ from vpn import models
if TYPE_CHECKING: if TYPE_CHECKING:
from core.graphql.filters import ContentTypeFilter from core.graphql.filters import ContentTypeFilter
from ipam.graphql.filters import IPAddressFilter, RouteTargetFilter from ipam.graphql.filters import IPAddressFilter, RouteTargetFilter
from netbox.graphql.filter_lookups import IntegerLookup from netbox.graphql.filter_lookups import BigIntegerLookup, IntegerLookup
from .enums import * from .enums import *
__all__ = ( __all__ = (
@@ -75,7 +75,7 @@ class TunnelFilter(TenancyFilterMixin, PrimaryModelFilter):
ipsec_profile: Annotated['IPSecProfileFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( ipsec_profile: Annotated['IPSecProfileFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
tunnel_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( tunnel_id: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
terminations: Annotated['TunnelTerminationFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( terminations: Annotated['TunnelTerminationFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
@@ -187,7 +187,7 @@ class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
type: BaseFilterLookup[Annotated['L2VPNTypeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = ( type: BaseFilterLookup[Annotated['L2VPNTypeEnum', strawberry.lazy('vpn.graphql.enums')]] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
identifier: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( identifier: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
import_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( import_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = (