From ef5bbdb1e24ec114f7718414510db11f2953b1b2 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 10 Dec 2021 11:38:58 -0500 Subject: [PATCH 1/6] Move available prefixes endpoint to its own view --- netbox/ipam/api/mixins.py | 84 ---------------------- netbox/ipam/api/urls.py | 9 ++- netbox/ipam/api/views.py | 143 ++++++++++++++++++++++++------------- netbox/netbox/api/views.py | 33 +++++---- 4 files changed, 118 insertions(+), 151 deletions(-) diff --git a/netbox/ipam/api/mixins.py b/netbox/ipam/api/mixins.py index 552c77d57..9d7f4e4d0 100644 --- a/netbox/ipam/api/mixins.py +++ b/netbox/ipam/api/mixins.py @@ -13,90 +13,6 @@ from utilities.constants import ADVISORY_LOCK_KEYS from . import serializers -class AvailablePrefixesMixin: - - @swagger_auto_schema(method='get', responses={200: serializers.AvailablePrefixSerializer(many=True)}) - @swagger_auto_schema(method='post', responses={201: serializers.PrefixSerializer(many=False)}) - @action(detail=True, url_path='available-prefixes', methods=['get', 'post']) - @advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes']) - def available_prefixes(self, request, pk=None): - """ - A convenience method for returning available child prefixes within a parent. - - The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being - invoked in parallel, which results in a race condition where multiple insertions can occur. - """ - prefix = get_object_or_404(self.queryset, pk=pk) - available_prefixes = prefix.get_available_prefixes() - - if request.method == 'POST': - - # Validate Requested Prefixes' length - serializer = serializers.PrefixLengthSerializer( - data=request.data if isinstance(request.data, list) else [request.data], - many=True, - context={ - 'request': request, - 'prefix': prefix, - } - ) - if not serializer.is_valid(): - return Response( - serializer.errors, - status=status.HTTP_400_BAD_REQUEST - ) - - requested_prefixes = serializer.validated_data - # Allocate prefixes to the requested objects based on availability within the parent - for i, requested_prefix in enumerate(requested_prefixes): - - # Find the first available prefix equal to or larger than the requested size - for available_prefix in available_prefixes.iter_cidrs(): - if requested_prefix['prefix_length'] >= available_prefix.prefixlen: - allocated_prefix = '{}/{}'.format(available_prefix.network, requested_prefix['prefix_length']) - requested_prefix['prefix'] = allocated_prefix - requested_prefix['vrf'] = prefix.vrf.pk if prefix.vrf else None - break - else: - return Response( - { - "detail": "Insufficient space is available to accommodate the requested prefix size(s)" - }, - status=status.HTTP_204_NO_CONTENT - ) - - # Remove the allocated prefix from the list of available prefixes - available_prefixes.remove(allocated_prefix) - - # Initialize the serializer with a list or a single object depending on what was requested - context = {'request': request} - if isinstance(request.data, list): - serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True, context=context) - else: - serializer = serializers.PrefixSerializer(data=requested_prefixes[0], context=context) - - # Create the new Prefix(es) - if serializer.is_valid(): - try: - with transaction.atomic(): - created = serializer.save() - self._validate_objects(created) - except ObjectDoesNotExist: - raise PermissionDenied() - return Response(serializer.data, status=status.HTTP_201_CREATED) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - else: - - serializer = serializers.AvailablePrefixSerializer(available_prefixes.iter_cidrs(), many=True, context={ - 'request': request, - 'vrf': prefix.vrf, - }) - - return Response(serializer.data) - - class AvailableIPsMixin: parent_model = Prefix diff --git a/netbox/ipam/api/urls.py b/netbox/ipam/api/urls.py index e465fbd89..a3bfcb330 100644 --- a/netbox/ipam/api/urls.py +++ b/netbox/ipam/api/urls.py @@ -1,3 +1,5 @@ +from django.urls import path + from netbox.api import OrderedDefaultRouter from . import views @@ -42,4 +44,9 @@ router.register('vlans', views.VLANViewSet) router.register('services', views.ServiceViewSet) app_name = 'ipam-api' -urlpatterns = router.urls + +urlpatterns = [ + path('prefixes//available-prefixes/', views.AvailablePrefixesView.as_view(), name='prefix-available-prefixes'), +] + +urlpatterns += router.urls diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index cdb40333d..b410c7b74 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -1,10 +1,19 @@ +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.db import transaction +from django_pglocks import advisory_lock +from django.shortcuts import get_object_or_404 +from rest_framework import status +from rest_framework.response import Response from rest_framework.routers import APIRootView +from rest_framework.views import APIView + from dcim.models import Site from extras.api.views import CustomFieldModelViewSet from ipam import filtersets from ipam.models import * -from netbox.api.views import ModelViewSet +from netbox.api.views import ModelViewSet, ObjectValidationMixin +from utilities.constants import ADVISORY_LOCK_KEYS from utilities.utils import count_related from . import mixins, serializers @@ -18,7 +27,7 @@ class IPAMRootView(APIRootView): # -# ASNs +# Viewsets # class ASNViewSet(CustomFieldModelViewSet): @@ -27,10 +36,6 @@ class ASNViewSet(CustomFieldModelViewSet): filterset_class = filtersets.ASNFilterSet -# -# VRFs -# - class VRFViewSet(CustomFieldModelViewSet): queryset = VRF.objects.prefetch_related('tenant').prefetch_related( 'import_targets', 'export_targets', 'tags' @@ -42,20 +47,12 @@ class VRFViewSet(CustomFieldModelViewSet): filterset_class = filtersets.VRFFilterSet -# -# Route targets -# - class RouteTargetViewSet(CustomFieldModelViewSet): queryset = RouteTarget.objects.prefetch_related('tenant').prefetch_related('tags') serializer_class = serializers.RouteTargetSerializer filterset_class = filtersets.RouteTargetFilterSet -# -# RIRs -# - class RIRViewSet(CustomFieldModelViewSet): queryset = RIR.objects.annotate( aggregate_count=count_related(Aggregate, 'rir') @@ -64,20 +61,12 @@ class RIRViewSet(CustomFieldModelViewSet): filterset_class = filtersets.RIRFilterSet -# -# Aggregates -# - class AggregateViewSet(CustomFieldModelViewSet): queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags') serializer_class = serializers.AggregateSerializer filterset_class = filtersets.AggregateFilterSet -# -# Roles -# - class RoleViewSet(CustomFieldModelViewSet): queryset = Role.objects.annotate( prefix_count=count_related(Prefix, 'role'), @@ -87,11 +76,7 @@ class RoleViewSet(CustomFieldModelViewSet): filterset_class = filtersets.RoleFilterSet -# -# Prefixes -# - -class PrefixViewSet(mixins.AvailableIPsMixin, mixins.AvailablePrefixesMixin, CustomFieldModelViewSet): +class PrefixViewSet(mixins.AvailableIPsMixin, CustomFieldModelViewSet): queryset = Prefix.objects.prefetch_related( 'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags' ) @@ -106,10 +91,6 @@ class PrefixViewSet(mixins.AvailableIPsMixin, mixins.AvailablePrefixesMixin, Cus return super().get_serializer_class() -# -# IP ranges -# - class IPRangeViewSet(mixins.AvailableIPsMixin, CustomFieldModelViewSet): queryset = IPRange.objects.prefetch_related('vrf', 'role', 'tenant', 'tags') serializer_class = serializers.IPRangeSerializer @@ -118,10 +99,6 @@ class IPRangeViewSet(mixins.AvailableIPsMixin, CustomFieldModelViewSet): parent_model = IPRange # AvailableIPsMixin -# -# IP addresses -# - class IPAddressViewSet(CustomFieldModelViewSet): queryset = IPAddress.objects.prefetch_related( 'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object' @@ -130,10 +107,6 @@ class IPAddressViewSet(CustomFieldModelViewSet): filterset_class = filtersets.IPAddressFilterSet -# -# FHRP groups -# - class FHRPGroupViewSet(CustomFieldModelViewSet): queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags') serializer_class = serializers.FHRPGroupSerializer @@ -147,10 +120,6 @@ class FHRPGroupAssignmentViewSet(CustomFieldModelViewSet): filterset_class = filtersets.FHRPGroupAssignmentFilterSet -# -# VLAN groups -# - class VLANGroupViewSet(CustomFieldModelViewSet): queryset = VLANGroup.objects.annotate( vlan_count=count_related(VLAN, 'group') @@ -159,10 +128,6 @@ class VLANGroupViewSet(CustomFieldModelViewSet): filterset_class = filtersets.VLANGroupFilterSet -# -# VLANs -# - class VLANViewSet(CustomFieldModelViewSet): queryset = VLAN.objects.prefetch_related( 'site', 'group', 'tenant', 'role', 'tags' @@ -173,13 +138,89 @@ class VLANViewSet(CustomFieldModelViewSet): filterset_class = filtersets.VLANFilterSet -# -# Services -# - class ServiceViewSet(ModelViewSet): queryset = Service.objects.prefetch_related( 'device', 'virtual_machine', 'tags', 'ipaddresses' ) serializer_class = serializers.ServiceSerializer filterset_class = filtersets.ServiceFilterSet + + +# +# Views +# + +class AvailablePrefixesView(ObjectValidationMixin, APIView): + queryset = Prefix.objects.all() + + def get(self, request, pk): + prefix = get_object_or_404(self.queryset, pk=pk) + available_prefixes = prefix.get_available_prefixes() + + serializer = serializers.AvailablePrefixSerializer(available_prefixes.iter_cidrs(), many=True, context={ + 'request': request, + 'vrf': prefix.vrf, + }) + + return Response(serializer.data) + + @advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes']) + def post(self, request, pk): + prefix = get_object_or_404(self.queryset, pk=pk) + available_prefixes = prefix.get_available_prefixes() + + # Validate Requested Prefixes' length + serializer = serializers.PrefixLengthSerializer( + data=request.data if isinstance(request.data, list) else [request.data], + many=True, + context={ + 'request': request, + 'prefix': prefix, + } + ) + if not serializer.is_valid(): + return Response( + serializer.errors, + status=status.HTTP_400_BAD_REQUEST + ) + + requested_prefixes = serializer.validated_data + # Allocate prefixes to the requested objects based on availability within the parent + for i, requested_prefix in enumerate(requested_prefixes): + + # Find the first available prefix equal to or larger than the requested size + for available_prefix in available_prefixes.iter_cidrs(): + if requested_prefix['prefix_length'] >= available_prefix.prefixlen: + allocated_prefix = '{}/{}'.format(available_prefix.network, requested_prefix['prefix_length']) + requested_prefix['prefix'] = allocated_prefix + requested_prefix['vrf'] = prefix.vrf.pk if prefix.vrf else None + break + else: + return Response( + { + "detail": "Insufficient space is available to accommodate the requested prefix size(s)" + }, + status=status.HTTP_204_NO_CONTENT + ) + + # Remove the allocated prefix from the list of available prefixes + available_prefixes.remove(allocated_prefix) + + # Initialize the serializer with a list or a single object depending on what was requested + context = {'request': request} + if isinstance(request.data, list): + serializer = serializers.PrefixSerializer(data=requested_prefixes, many=True, context=context) + else: + serializer = serializers.PrefixSerializer(data=requested_prefixes[0], context=context) + + # Create the new Prefix(es) + if serializer.is_valid(): + try: + with transaction.atomic(): + created = serializer.save() + self._validate_objects(created) + except ObjectDoesNotExist: + raise PermissionDenied() + return Response(serializer.data, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/netbox/netbox/api/views.py b/netbox/netbox/api/views.py index 7ad64aeae..2df0a4c83 100644 --- a/netbox/netbox/api/views.py +++ b/netbox/netbox/api/views.py @@ -123,11 +123,28 @@ class BulkDestroyModelMixin: self.perform_destroy(obj) +class ObjectValidationMixin: + + def _validate_objects(self, instance): + """ + Check that the provided instance or list of instances are matched by the current queryset. This confirms that + any newly created or modified objects abide by the attributes granted by any applicable ObjectPermissions. + """ + if type(instance) is list: + # Check that all instances are still included in the view's queryset + conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count() + if conforming_count != len(instance): + raise ObjectDoesNotExist + else: + # Check that the instance is matched by the view's queryset + self.queryset.get(pk=instance.pk) + + # # Viewsets # -class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_): +class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectValidationMixin, ModelViewSet_): """ Extend DRF's ModelViewSet to support bulk update and delete functions. """ @@ -211,20 +228,6 @@ class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ModelViewSet_): **kwargs ) - def _validate_objects(self, instance): - """ - Check that the provided instance or list of instances are matched by the current queryset. This confirms that - any newly created or modified objects abide by the attributes granted by any applicable ObjectPermissions. - """ - if type(instance) is list: - # Check that all instances are still included in the view's queryset - conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count() - if conforming_count != len(instance): - raise ObjectDoesNotExist - else: - # Check that the instance is matched by the view's queryset - self.queryset.get(pk=instance.pk) - def list(self, request, *args, **kwargs): """ Overrides ListModelMixin to allow processing ExportTemplates. From 35eabc0353f282a6ed56690dc13570ae5df136b2 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 10 Dec 2021 12:37:55 -0500 Subject: [PATCH 2/6] Move available IPs endpoints to separate views --- netbox/ipam/api/mixins.py | 101 -------------------------------------- netbox/ipam/api/urls.py | 17 ++++++- netbox/ipam/api/views.py | 100 +++++++++++++++++++++++++++++++++++-- 3 files changed, 111 insertions(+), 107 deletions(-) delete mode 100644 netbox/ipam/api/mixins.py diff --git a/netbox/ipam/api/mixins.py b/netbox/ipam/api/mixins.py deleted file mode 100644 index 9d7f4e4d0..000000000 --- a/netbox/ipam/api/mixins.py +++ /dev/null @@ -1,101 +0,0 @@ -from django.core.exceptions import ObjectDoesNotExist, PermissionDenied -from django.db import transaction -from django.shortcuts import get_object_or_404 -from django_pglocks import advisory_lock -from drf_yasg.utils import swagger_auto_schema -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.response import Response - -from ipam.models import * -from netbox.config import get_config -from utilities.constants import ADVISORY_LOCK_KEYS -from . import serializers - - -class AvailableIPsMixin: - parent_model = Prefix - - @swagger_auto_schema(method='get', responses={200: serializers.AvailableIPSerializer(many=True)}) - @swagger_auto_schema(method='post', responses={201: serializers.AvailableIPSerializer(many=True)}, - request_body=serializers.AvailableIPSerializer(many=True)) - @action(detail=True, url_path='available-ips', methods=['get', 'post'], queryset=IPAddress.objects.all()) - @advisory_lock(ADVISORY_LOCK_KEYS['available-ips']) - def available_ips(self, request, pk=None): - """ - A convenience method for returning available IP addresses within a Prefix or IPRange. By default, the number of - IPs returned will be equivalent to PAGINATE_COUNT. An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be - passed, however results will not be paginated. - - The advisory lock decorator uses a PostgreSQL advisory lock to prevent this API from being - invoked in parallel, which results in a race condition where multiple insertions can occur. - """ - parent = get_object_or_404(self.parent_model.objects.restrict(request.user), pk=pk) - - # Create the next available IP - if request.method == 'POST': - - # Normalize to a list of objects - requested_ips = request.data if isinstance(request.data, list) else [request.data] - - # Determine if the requested number of IPs is available - available_ips = parent.get_available_ips() - if available_ips.size < len(requested_ips): - return Response( - { - "detail": f"An insufficient number of IP addresses are available within {parent} " - f"({len(requested_ips)} requested, {len(available_ips)} available)" - }, - status=status.HTTP_204_NO_CONTENT - ) - - # Assign addresses from the list of available IPs and copy VRF assignment from the parent - available_ips = iter(available_ips) - for requested_ip in requested_ips: - requested_ip['address'] = f'{next(available_ips)}/{parent.mask_length}' - requested_ip['vrf'] = parent.vrf.pk if parent.vrf else None - - # Initialize the serializer with a list or a single object depending on what was requested - context = {'request': request} - if isinstance(request.data, list): - serializer = serializers.IPAddressSerializer(data=requested_ips, many=True, context=context) - else: - serializer = serializers.IPAddressSerializer(data=requested_ips[0], context=context) - - # Create the new IP address(es) - if serializer.is_valid(): - try: - with transaction.atomic(): - created = serializer.save() - self._validate_objects(created) - except ObjectDoesNotExist: - raise PermissionDenied() - return Response(serializer.data, status=status.HTTP_201_CREATED) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - # Determine the maximum number of IPs to return - else: - config = get_config() - PAGINATE_COUNT = config.PAGINATE_COUNT - MAX_PAGE_SIZE = config.MAX_PAGE_SIZE - try: - limit = int(request.query_params.get('limit', PAGINATE_COUNT)) - except ValueError: - limit = PAGINATE_COUNT - if MAX_PAGE_SIZE: - limit = min(limit, MAX_PAGE_SIZE) - - # Calculate available IPs within the parent - ip_list = [] - for index, ip in enumerate(parent.get_available_ips(), start=1): - ip_list.append(ip) - if index == limit: - break - serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={ - 'request': request, - 'parent': parent, - 'vrf': parent.vrf, - }) - - return Response(serializer.data) diff --git a/netbox/ipam/api/urls.py b/netbox/ipam/api/urls.py index a3bfcb330..26a36325f 100644 --- a/netbox/ipam/api/urls.py +++ b/netbox/ipam/api/urls.py @@ -1,6 +1,7 @@ from django.urls import path from netbox.api import OrderedDefaultRouter +from ipam.models import IPRange, Prefix from . import views @@ -46,7 +47,21 @@ router.register('services', views.ServiceViewSet) app_name = 'ipam-api' urlpatterns = [ - path('prefixes//available-prefixes/', views.AvailablePrefixesView.as_view(), name='prefix-available-prefixes'), + path( + 'ip-ranges//available-ips/', + views.IPRangeAvailableIPAddressesView.as_view(), + name='iprange-available-ips' + ), + path( + 'prefixes//available-prefixes/', + views.AvailablePrefixesView.as_view(), + name='prefix-available-prefixes' + ), + path( + 'prefixes//available-ips/', + views.PrefixAvailableIPAddressesView.as_view(), + name='prefix-available-ips' + ), ] urlpatterns += router.urls diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index b410c7b74..98420826a 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -13,9 +13,10 @@ from extras.api.views import CustomFieldModelViewSet from ipam import filtersets from ipam.models import * from netbox.api.views import ModelViewSet, ObjectValidationMixin +from netbox.config import get_config from utilities.constants import ADVISORY_LOCK_KEYS from utilities.utils import count_related -from . import mixins, serializers +from . import serializers class IPAMRootView(APIRootView): @@ -76,7 +77,7 @@ class RoleViewSet(CustomFieldModelViewSet): filterset_class = filtersets.RoleFilterSet -class PrefixViewSet(mixins.AvailableIPsMixin, CustomFieldModelViewSet): +class PrefixViewSet(CustomFieldModelViewSet): queryset = Prefix.objects.prefetch_related( 'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags' ) @@ -91,7 +92,7 @@ class PrefixViewSet(mixins.AvailableIPsMixin, CustomFieldModelViewSet): return super().get_serializer_class() -class IPRangeViewSet(mixins.AvailableIPsMixin, CustomFieldModelViewSet): +class IPRangeViewSet(CustomFieldModelViewSet): queryset = IPRange.objects.prefetch_related('vrf', 'role', 'tenant', 'tags') serializer_class = serializers.IPRangeSerializer filterset_class = filtersets.IPRangeFilterSet @@ -154,7 +155,7 @@ class AvailablePrefixesView(ObjectValidationMixin, APIView): queryset = Prefix.objects.all() def get(self, request, pk): - prefix = get_object_or_404(self.queryset, pk=pk) + prefix = get_object_or_404(self.queryset.restrict(request.user), pk=pk) available_prefixes = prefix.get_available_prefixes() serializer = serializers.AvailablePrefixSerializer(available_prefixes.iter_cidrs(), many=True, context={ @@ -166,7 +167,7 @@ class AvailablePrefixesView(ObjectValidationMixin, APIView): @advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes']) def post(self, request, pk): - prefix = get_object_or_404(self.queryset, pk=pk) + prefix = get_object_or_404(self.queryset.restrict(request.user), pk=pk) available_prefixes = prefix.get_available_prefixes() # Validate Requested Prefixes' length @@ -224,3 +225,92 @@ class AvailablePrefixesView(ObjectValidationMixin, APIView): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class AvailableIPAddressesView(ObjectValidationMixin, APIView): + queryset = IPAddress.objects.all() + + def get_parent(self, request, pk): + raise NotImplemented() + + def get(self, request, pk): + parent = self.get_parent(request, pk) + config = get_config() + PAGINATE_COUNT = config.PAGINATE_COUNT + MAX_PAGE_SIZE = config.MAX_PAGE_SIZE + + try: + limit = int(request.query_params.get('limit', PAGINATE_COUNT)) + except ValueError: + limit = PAGINATE_COUNT + if MAX_PAGE_SIZE: + limit = min(limit, MAX_PAGE_SIZE) + + # Calculate available IPs within the parent + ip_list = [] + for index, ip in enumerate(parent.get_available_ips(), start=1): + ip_list.append(ip) + if index == limit: + break + serializer = serializers.AvailableIPSerializer(ip_list, many=True, context={ + 'request': request, + 'parent': parent, + 'vrf': parent.vrf, + }) + + return Response(serializer.data) + + @advisory_lock(ADVISORY_LOCK_KEYS['available-ips']) + def post(self, request, pk): + parent = self.get_parent(request, pk) + + # Normalize to a list of objects + requested_ips = request.data if isinstance(request.data, list) else [request.data] + + # Determine if the requested number of IPs is available + available_ips = parent.get_available_ips() + if available_ips.size < len(requested_ips): + return Response( + { + "detail": f"An insufficient number of IP addresses are available within {parent} " + f"({len(requested_ips)} requested, {len(available_ips)} available)" + }, + status=status.HTTP_204_NO_CONTENT + ) + + # Assign addresses from the list of available IPs and copy VRF assignment from the parent + available_ips = iter(available_ips) + for requested_ip in requested_ips: + requested_ip['address'] = f'{next(available_ips)}/{parent.mask_length}' + requested_ip['vrf'] = parent.vrf.pk if parent.vrf else None + + # Initialize the serializer with a list or a single object depending on what was requested + context = {'request': request} + if isinstance(request.data, list): + serializer = serializers.IPAddressSerializer(data=requested_ips, many=True, context=context) + else: + serializer = serializers.IPAddressSerializer(data=requested_ips[0], context=context) + + # Create the new IP address(es) + if serializer.is_valid(): + try: + with transaction.atomic(): + created = serializer.save() + self._validate_objects(created) + except ObjectDoesNotExist: + raise PermissionDenied() + return Response(serializer.data, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class PrefixAvailableIPAddressesView(AvailableIPAddressesView): + + def get_parent(self, request, pk): + return get_object_or_404(Prefix.objects.restrict(request.user), pk=pk) + + +class IPRangeAvailableIPAddressesView(AvailableIPAddressesView): + + def get_parent(self, request, pk): + return get_object_or_404(IPRange.objects.restrict(request.user), pk=pk) From 661b3c4bfbe4b9898366b45171240590b124434c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 10 Dec 2021 12:52:48 -0500 Subject: [PATCH 3/6] Fix queryset restrictions --- netbox/ipam/api/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 98420826a..fe828e520 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -155,7 +155,7 @@ class AvailablePrefixesView(ObjectValidationMixin, APIView): queryset = Prefix.objects.all() def get(self, request, pk): - prefix = get_object_or_404(self.queryset.restrict(request.user), pk=pk) + prefix = get_object_or_404(Prefix.objects.restrict(request.user), pk=pk) available_prefixes = prefix.get_available_prefixes() serializer = serializers.AvailablePrefixSerializer(available_prefixes.iter_cidrs(), many=True, context={ @@ -167,7 +167,8 @@ class AvailablePrefixesView(ObjectValidationMixin, APIView): @advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes']) def post(self, request, pk): - prefix = get_object_or_404(self.queryset.restrict(request.user), pk=pk) + self.queryset = self.queryset.restrict(request.user, 'add') + prefix = get_object_or_404(Prefix.objects.restrict(request.user), pk=pk) available_prefixes = prefix.get_available_prefixes() # Validate Requested Prefixes' length @@ -262,6 +263,7 @@ class AvailableIPAddressesView(ObjectValidationMixin, APIView): @advisory_lock(ADVISORY_LOCK_KEYS['available-ips']) def post(self, request, pk): + self.queryset = self.queryset.restrict(request.user, 'add') parent = self.get_parent(request, pk) # Normalize to a list of objects From 91fe158c263b9e0a485a7c35a3ed3c9973d7b163 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 10 Dec 2021 13:23:49 -0500 Subject: [PATCH 4/6] Restore endpoint schema documentation --- netbox/ipam/api/views.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index fe828e520..f3937c7d6 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -2,6 +2,7 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction from django_pglocks import advisory_lock from django.shortcuts import get_object_or_404 +from drf_yasg.utils import swagger_auto_schema from rest_framework import status from rest_framework.response import Response from rest_framework.routers import APIRootView @@ -154,6 +155,7 @@ class ServiceViewSet(ModelViewSet): class AvailablePrefixesView(ObjectValidationMixin, APIView): queryset = Prefix.objects.all() + @swagger_auto_schema(responses={200: serializers.AvailablePrefixSerializer(many=True)}) def get(self, request, pk): prefix = get_object_or_404(Prefix.objects.restrict(request.user), pk=pk) available_prefixes = prefix.get_available_prefixes() @@ -165,6 +167,10 @@ class AvailablePrefixesView(ObjectValidationMixin, APIView): return Response(serializer.data) + @swagger_auto_schema( + request_body=serializers.PrefixLengthSerializer, + responses={201: serializers.PrefixSerializer(many=True)} + ) @advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes']) def post(self, request, pk): self.queryset = self.queryset.restrict(request.user, 'add') @@ -234,6 +240,7 @@ class AvailableIPAddressesView(ObjectValidationMixin, APIView): def get_parent(self, request, pk): raise NotImplemented() + @swagger_auto_schema(responses={200: serializers.AvailableIPSerializer(many=True)}) def get(self, request, pk): parent = self.get_parent(request, pk) config = get_config() @@ -261,6 +268,10 @@ class AvailableIPAddressesView(ObjectValidationMixin, APIView): return Response(serializer.data) + @swagger_auto_schema( + request_body=serializers.AvailableIPSerializer, + responses={201: serializers.IPAddressSerializer(many=True)} + ) @advisory_lock(ADVISORY_LOCK_KEYS['available-ips']) def post(self, request, pk): self.queryset = self.queryset.restrict(request.user, 'add') From 08de6c32c94d62d0bf46d78ef31e8d829cb4838e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 10 Dec 2021 13:26:56 -0500 Subject: [PATCH 5/6] Changelog for #5869 --- docs/release-notes/version-3.1.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 7e2305e6f..fb02055f2 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#5869](https://github.com/netbox-community/netbox/issues/5869) - Fix permissions evaluation under available prefix/IP REST API endpoints * [#7990](https://github.com/netbox-community/netbox/issues/7990) - Fix `title` display on contact detail view * [#7996](https://github.com/netbox-community/netbox/issues/7996) - Show WWN field in interface creation form * [#8001](https://github.com/netbox-community/netbox/issues/8001) - Correct verbose name for wireless LAN group model From d850b3ac7eefeb5e70b4941cde3d408c5338dfde Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 10 Dec 2021 13:58:11 -0500 Subject: [PATCH 6/6] Fix available prefix creation test --- netbox/ipam/tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 50eb64060..c3875420f 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -289,7 +289,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase): vrf = VRF.objects.create(name='VRF 1') prefix = Prefix.objects.create(prefix=IPNetwork('192.0.2.0/28'), vrf=vrf, is_pool=True) url = reverse('ipam-api:prefix-available-prefixes', kwargs={'pk': prefix.pk}) - self.add_permissions('ipam.add_prefix') + self.add_permissions('ipam.view_prefix', 'ipam.add_prefix') # Create four available prefixes with individual requests prefixes_to_be_created = [