nautobot 1.6.22__py3-none-any.whl → 1.6.24__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

@@ -330,13 +330,13 @@ class DynamicGroupViewSet(ModelViewSet, NotesViewSetMixin):
330
330
  # @extend_schema(methods=["get"], responses={200: member_response})
331
331
  @action(detail=True, methods=["get"])
332
332
  def members(self, request, pk, *args, **kwargs):
333
- """List member objects of the same type as the `content_type` for this dynamic group."""
333
+ """List the member objects of this dynamic group."""
334
334
  instance = get_object_or_404(self.queryset, pk=pk)
335
335
 
336
336
  # Retrieve the serializer for the content_type and paginate the results
337
337
  member_model_class = instance.content_type.model_class()
338
338
  member_serializer_class = get_serializer_for_model(member_model_class)
339
- members = self.paginate_queryset(instance.members)
339
+ members = self.paginate_queryset(instance.members.restrict(request.user, "view"))
340
340
  member_serializer = member_serializer_class(members, many=True, context={"request": request})
341
341
  return self.get_paginated_response(member_serializer.data)
342
342
 
@@ -69,6 +69,7 @@ from nautobot.ipam.factory import VLANFactory
69
69
  from nautobot.ipam.models import VLAN, VLANGroup
70
70
  from nautobot.users.models import ObjectPermission
71
71
  from nautobot.utilities.choices import ColorChoices
72
+ from nautobot.utilities.permissions import get_permission_for_model
72
73
  from nautobot.utilities.testing import APITestCase, APIViewTestCases
73
74
  from nautobot.utilities.testing.utils import disable_warnings
74
75
  from nautobot.utilities.utils import get_route_for_model, slugify_dashes_to_underscores
@@ -752,13 +753,34 @@ class DynamicGroupTest(DynamicGroupTestMixin, APIViewTestCases.APIViewTestCase):
752
753
  def test_get_members(self):
753
754
  """Test that the `/members/` API endpoint returns what is expected."""
754
755
  self.add_permissions("extras.view_dynamicgroup")
755
- instance = DynamicGroup.objects.first()
756
+ instance = self.groups[0]
757
+ self.add_permissions(get_permission_for_model(instance.content_type.model_class(), "view"))
756
758
  member_count = instance.members.count()
757
759
  url = reverse("extras-api:dynamicgroup-members", kwargs={"pk": instance.pk})
758
760
  response = self.client.get(url, **self.header)
759
761
  self.assertHttpStatus(response, status.HTTP_200_OK)
760
762
  self.assertEqual(member_count, len(response.json()["results"]))
761
763
 
764
+ def test_get_members_with_constrained_permission(self):
765
+ """Test that the `/members/` API endpoint enforces permissions on the member model."""
766
+ self.add_permissions("extras.view_dynamicgroup")
767
+ instance = self.groups[0]
768
+ obj1 = instance.members.first()
769
+ obj_perm = ObjectPermission(
770
+ name="Test permission",
771
+ constraints={"pk__in": [obj1.pk]},
772
+ actions=["view"],
773
+ )
774
+ obj_perm.save()
775
+ obj_perm.users.add(self.user)
776
+ obj_perm.object_types.add(instance.content_type)
777
+
778
+ url = reverse("extras-api:dynamicgroup-members", kwargs={"pk": instance.pk})
779
+ response = self.client.get(url, **self.header)
780
+ self.assertHttpStatus(response, status.HTTP_200_OK)
781
+ self.assertEqual(len(response.json()["results"]), 1)
782
+ self.assertEqual(response.json()["results"][0]["id"], str(obj1.pk))
783
+
762
784
 
763
785
  class DynamicGroupMembershipTest(DynamicGroupTestMixin, APIViewTestCases.APIViewTestCase):
764
786
  model = DynamicGroupMembership
@@ -1002,7 +1002,7 @@ class DynamicGroupModelTest(DynamicGroupTestBase):
1002
1002
  group.members_cached
1003
1003
  self.assertEqual(mock_get_queryset.call_count, 1)
1004
1004
 
1005
- time.sleep(2) # Let the cache expire
1005
+ time.sleep(3) # Let the cache expire
1006
1006
 
1007
1007
  group.members_cached
1008
1008
  self.assertEqual(mock_get_queryset.call_count, 2)
@@ -66,6 +66,7 @@ from nautobot.extras.utils import get_job_content_type, TaggableClassesQuery
66
66
  from nautobot.ipam.factory import VLANFactory
67
67
  from nautobot.ipam.models import VLAN, VLANGroup
68
68
  from nautobot.users.models import ObjectPermission
69
+ from nautobot.utilities.permissions import get_permission_for_model
69
70
  from nautobot.utilities.testing import ViewTestCases, TestCase, extract_page_body, extract_form_failures
70
71
  from nautobot.utilities.testing.utils import disable_warnings, post_data
71
72
  from nautobot.utilities.utils import slugify_dashes_to_underscores
@@ -614,9 +615,11 @@ class DynamicGroupTestCase(
614
615
  content_type = ContentType.objects.get_for_model(Device)
615
616
 
616
617
  # DynamicGroup objects to test.
617
- DynamicGroup.objects.create(name="DG 1", slug="dg-1", content_type=content_type)
618
- DynamicGroup.objects.create(name="DG 2", slug="dg-2", content_type=content_type)
619
- DynamicGroup.objects.create(name="DG 3", slug="dg-3", content_type=content_type)
618
+ cls.dynamic_groups = [
619
+ DynamicGroup.objects.create(name="DG 1", slug="dg-1", content_type=content_type),
620
+ DynamicGroup.objects.create(name="DG 2", slug="dg-2", content_type=content_type),
621
+ DynamicGroup.objects.create(name="DG 3", slug="dg-3", content_type=content_type),
622
+ ]
620
623
 
621
624
  manufacturer = Manufacturer.objects.create(name="Manufacturer 1", slug="manufacturer-1")
622
625
  devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1", slug="device-type-1")
@@ -637,6 +640,38 @@ class DynamicGroupTestCase(
637
640
  "dynamic_group_memberships-MAX_NUM_FORMS": "1000",
638
641
  }
639
642
 
643
+ def test_get_object_with_permission(self):
644
+ instance = self._get_queryset().first()
645
+ # Add view permissions for the group's members:
646
+ self.add_permissions(get_permission_for_model(instance.content_type.model_class(), "view"))
647
+
648
+ response = super().test_get_object_with_permission()
649
+
650
+ response_body = extract_page_body(response.content.decode(response.charset))
651
+ # Check that the "members" table in the detail view includes all appropriate member objects
652
+ for member in instance.members:
653
+ self.assertIn(str(member.pk), response_body)
654
+
655
+ def test_get_object_with_constrained_permission(self):
656
+ instance = self._get_queryset().first()
657
+ # Add view permission for one of the group's members but not the others:
658
+ member1, member2 = instance.members[:2]
659
+ obj_perm = ObjectPermission(
660
+ name="Members permission",
661
+ constraints={"pk": member1.pk},
662
+ actions=["view"],
663
+ )
664
+ obj_perm.save()
665
+ obj_perm.users.add(self.user)
666
+ obj_perm.object_types.add(instance.content_type)
667
+
668
+ response = super().test_get_object_with_constrained_permission()
669
+
670
+ response_body = extract_page_body(response.content.decode(response.charset))
671
+ # Check that the "members" table in the detail view includes all permitted member objects
672
+ self.assertIn(str(member1.pk), response_body)
673
+ self.assertNotIn(str(member2.pk), response_body)
674
+
640
675
  def test_get_object_dynamic_groups_anonymous(self):
641
676
  url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
642
677
  self.client.logout()
@@ -660,7 +695,6 @@ class DynamicGroupTestCase(
660
695
  self.assertIn("DG 3", response_body, msg=response_body)
661
696
 
662
697
  def test_get_object_dynamic_groups_with_constrained_permission(self):
663
- self.add_permissions("extras.view_dynamicgroup")
664
698
  obj_perm = ObjectPermission(
665
699
  name="View a device",
666
700
  constraints={"pk": Device.objects.first().pk},
@@ -669,12 +703,22 @@ class DynamicGroupTestCase(
669
703
  obj_perm.save()
670
704
  obj_perm.users.add(self.user)
671
705
  obj_perm.object_types.add(ContentType.objects.get_for_model(Device))
706
+ obj_perm_2 = ObjectPermission(
707
+ name="View a Dynamic Group",
708
+ constraints={"pk": self.dynamic_groups[0].pk},
709
+ actions=["view"],
710
+ )
711
+ obj_perm_2.save()
712
+ obj_perm_2.users.add(self.user)
713
+ obj_perm_2.object_types.add(ContentType.objects.get_for_model(DynamicGroup))
672
714
 
673
715
  url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.first().pk})
674
716
  response = self.client.get(url)
675
717
  self.assertHttpStatus(response, 200)
676
718
  response_body = response.content.decode(response.charset)
677
719
  self.assertIn("DG 1", response_body, msg=response_body)
720
+ self.assertNotIn("DG 2", response_body, msg=response_body)
721
+ self.assertNotIn("DG 3", response_body, msg=response_body)
678
722
 
679
723
  url = reverse("dcim:device_dynamicgroups", kwargs={"pk": Device.objects.last().pk})
680
724
  response = self.client.get(url)
nautobot/extras/views.py CHANGED
@@ -550,7 +550,7 @@ class DynamicGroupView(generic.ObjectView):
550
550
 
551
551
  if table_class is not None:
552
552
  # Members table (for display on Members nav tab)
553
- members_table = table_class(instance.members, orderable=False)
553
+ members_table = table_class(instance.members.restrict(request.user, "view"), orderable=False)
554
554
  paginate = {
555
555
  "paginator_class": EnhancedPaginator,
556
556
  "per_page": get_paginate_count(request),
@@ -722,7 +722,9 @@ class ObjectDynamicGroupsView(generic.GenericView):
722
722
  obj = get_object_or_404(model, **kwargs)
723
723
 
724
724
  # Gather all dynamic groups for this object (and its related objects)
725
- dynamicsgroups_table = tables.DynamicGroupTable(data=obj.dynamic_groups_cached, orderable=False)
725
+ dynamicsgroups_table = tables.DynamicGroupTable(
726
+ data=obj.dynamic_groups_cached.restrict(request.user, "view"), orderable=False
727
+ )
726
728
 
727
729
  # Apply the request context
728
730
  paginate = {